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/click_house/migration_support/migration_context_spec.rb233
-rw-r--r--spec/components/projects/ml/models_index_component_spec.rb11
-rw-r--r--spec/components/projects/ml/show_ml_model_component_spec.rb7
-rw-r--r--spec/components/projects/ml/show_ml_model_version_component_spec.rb35
-rw-r--r--spec/controllers/admin/application_settings_controller_spec.rb94
-rw-r--r--spec/controllers/autocomplete_controller_spec.rb106
-rw-r--r--spec/controllers/groups/settings/applications_controller_spec.rb357
-rw-r--r--spec/controllers/import/bulk_imports_controller_spec.rb27
-rw-r--r--spec/controllers/oauth/jira_dvcs/authorizations_controller_spec.rb71
-rw-r--r--spec/controllers/profiles/notifications_controller_spec.rb2
-rw-r--r--spec/controllers/profiles_controller_spec.rb10
-rw-r--r--spec/controllers/projects/artifacts_controller_spec.rb28
-rw-r--r--spec/controllers/projects/blob_controller_spec.rb28
-rw-r--r--spec/controllers/projects/group_links_controller_spec.rb56
-rw-r--r--spec/controllers/projects/jobs_controller_spec.rb58
-rw-r--r--spec/controllers/projects/merge_requests/drafts_controller_spec.rb45
-rw-r--r--spec/controllers/projects/merge_requests_controller_spec.rb147
-rw-r--r--spec/controllers/projects/pages_controller_spec.rb12
-rw-r--r--spec/controllers/projects/tree_controller_spec.rb30
-rw-r--r--spec/controllers/repositories/git_http_controller_spec.rb20
-rw-r--r--spec/controllers/search_controller_spec.rb28
-rw-r--r--spec/db/docs_spec.rb7
-rw-r--r--spec/db/migration_spec.rb3
-rw-r--r--spec/experiments/ios_specific_templates_experiment_spec.rb62
-rw-r--r--spec/factories/activity_pub/releases_subscriptions.rb26
-rw-r--r--spec/factories/ai/service_access_tokens.rb5
-rw-r--r--spec/factories/ci/build_trace_chunks.rb16
-rw-r--r--spec/factories/ci/builds.rb5
-rw-r--r--spec/factories/ci/catalog/resources/components.rb2
-rw-r--r--spec/factories/ci/pipeline_artifacts.rb8
-rw-r--r--spec/factories/ci/reports/security/findings.rb2
-rw-r--r--spec/factories/commit_statuses.rb4
-rw-r--r--spec/factories/ml/model_metadata.rb10
-rw-r--r--spec/factories/ml/models.rb6
-rw-r--r--spec/factories/packages/npm/metadata_cache.rb10
-rw-r--r--spec/factories/packages/nuget/symbol.rb1
-rw-r--r--spec/factories/pages_domains.rb81
-rw-r--r--spec/factories/projects.rb33
-rw-r--r--spec/factories/snippet_repositories.rb8
-rw-r--r--spec/factories/terraform/state_version.rb8
-rw-r--r--spec/factories/users.rb4
-rw-r--r--spec/factories/users/phone_number_validations.rb1
-rw-r--r--spec/fast_spec_helper.rb1
-rw-r--r--spec/features/abuse_report_spec.rb120
-rw-r--r--spec/features/admin/admin_browse_spam_logs_spec.rb14
-rw-r--r--spec/features/admin/admin_deploy_keys_spec.rb10
-rw-r--r--spec/features/admin/admin_dev_ops_reports_spec.rb4
-rw-r--r--spec/features/admin/admin_groups_spec.rb2
-rw-r--r--spec/features/admin/admin_hooks_spec.rb6
-rw-r--r--spec/features/admin/admin_jobs_spec.rb2
-rw-r--r--spec/features/admin/admin_mode/logout_spec.rb15
-rw-r--r--spec/features/admin/admin_mode/workers_spec.rb6
-rw-r--r--spec/features/admin/admin_mode_spec.rb47
-rw-r--r--spec/features/admin/admin_projects_spec.rb2
-rw-r--r--spec/features/admin/admin_runners_spec.rb98
-rw-r--r--spec/features/admin/admin_sees_background_migrations_spec.rb16
-rw-r--r--spec/features/admin/admin_settings_spec.rb148
-rw-r--r--spec/features/admin/admin_users_spec.rb8
-rw-r--r--spec/features/admin/admin_uses_repository_checks_spec.rb2
-rw-r--r--spec/features/admin/broadcast_messages_spec.rb8
-rw-r--r--spec/features/admin/users/user_spec.rb26
-rw-r--r--spec/features/admin/users/users_spec.rb12
-rw-r--r--spec/features/admin_variables_spec.rb13
-rw-r--r--spec/features/alert_management/alert_details_spec.rb12
-rw-r--r--spec/features/alert_management/alert_management_list_spec.rb6
-rw-r--r--spec/features/boards/board_filters_spec.rb3
-rw-r--r--spec/features/boards/boards_spec.rb9
-rw-r--r--spec/features/boards/issue_ordering_spec.rb3
-rw-r--r--spec/features/boards/multiple_boards_spec.rb2
-rw-r--r--spec/features/boards/new_issue_spec.rb8
-rw-r--r--spec/features/boards/reload_boards_on_browser_back_spec.rb2
-rw-r--r--spec/features/boards/sidebar_assignee_spec.rb2
-rw-r--r--spec/features/boards/sidebar_labels_in_namespaces_spec.rb2
-rw-r--r--spec/features/boards/sidebar_labels_spec.rb1
-rw-r--r--spec/features/boards/sidebar_spec.rb1
-rw-r--r--spec/features/boards/user_adds_lists_to_board_spec.rb1
-rw-r--r--spec/features/boards/user_visits_board_spec.rb3
-rw-r--r--spec/features/broadcast_messages_spec.rb8
-rw-r--r--spec/features/calendar_spec.rb3
-rw-r--r--spec/features/callouts/registration_enabled_spec.rb2
-rw-r--r--spec/features/clusters/create_agent_spec.rb2
-rw-r--r--spec/features/commit_spec.rb8
-rw-r--r--spec/features/commits_spec.rb10
-rw-r--r--spec/features/contextual_sidebar_spec.rb32
-rw-r--r--spec/features/dashboard/activity_spec.rb6
-rw-r--r--spec/features/dashboard/group_dashboard_with_external_authorization_service_spec.rb43
-rw-r--r--spec/features/dashboard/group_spec.rb2
-rw-r--r--spec/features/dashboard/groups_list_spec.rb2
-rw-r--r--spec/features/dashboard/issuables_counter_spec.rb36
-rw-r--r--spec/features/dashboard/issues_spec.rb20
-rw-r--r--spec/features/dashboard/merge_requests_spec.rb46
-rw-r--r--spec/features/dashboard/milestones_spec.rb12
-rw-r--r--spec/features/dashboard/navbar_spec.rb4
-rw-r--r--spec/features/dashboard/projects_spec.rb16
-rw-r--r--spec/features/dashboard/shortcuts_spec.rb1
-rw-r--r--spec/features/dashboard/snippets_spec.rb8
-rw-r--r--spec/features/dashboard/todos/todos_spec.rb16
-rw-r--r--spec/features/explore/catalog_spec.rb80
-rw-r--r--spec/features/explore/navbar_spec.rb15
-rw-r--r--spec/features/explore/user_explores_projects_spec.rb35
-rw-r--r--spec/features/global_search_spec.rb17
-rw-r--r--spec/features/group_variables_spec.rb15
-rw-r--r--spec/features/groups/board_sidebar_spec.rb1
-rw-r--r--spec/features/groups/board_spec.rb4
-rw-r--r--spec/features/groups/container_registry_spec.rb6
-rw-r--r--spec/features/groups/dependency_proxy_spec.rb8
-rw-r--r--spec/features/groups/group_page_with_external_authorization_service_spec.rb16
-rw-r--r--spec/features/groups/group_settings_spec.rb2
-rw-r--r--spec/features/groups/members/request_access_spec.rb11
-rw-r--r--spec/features/groups/members/sort_members_spec.rb2
-rw-r--r--spec/features/groups/merge_requests_spec.rb6
-rw-r--r--spec/features/groups/navbar_spec.rb14
-rw-r--r--spec/features/groups/new_group_page_spec.rb41
-rw-r--r--spec/features/groups/packages_spec.rb4
-rw-r--r--spec/features/groups/settings/packages_and_registries_spec.rb32
-rw-r--r--spec/features/groups/show_spec.rb2
-rw-r--r--spec/features/groups/user_sees_package_sidebar_spec.rb15
-rw-r--r--spec/features/groups_spec.rb25
-rw-r--r--spec/features/help_dropdown_spec.rb89
-rw-r--r--spec/features/ide/user_opens_merge_request_spec.rb6
-rw-r--r--spec/features/invites_spec.rb3
-rw-r--r--spec/features/issuables/shortcuts_issuable_spec.rb4
-rw-r--r--spec/features/issues/discussion_lock_spec.rb6
-rw-r--r--spec/features/issues/filtered_search/visual_tokens_spec.rb6
-rw-r--r--spec/features/issues/form_spec.rb24
-rw-r--r--spec/features/issues/issue_sidebar_spec.rb183
-rw-r--r--spec/features/issues/issue_state_spec.rb4
-rw-r--r--spec/features/issues/move_spec.rb4
-rw-r--r--spec/features/issues/service_desk_spec.rb8
-rw-r--r--spec/features/issues/todo_spec.rb10
-rw-r--r--spec/features/issues/user_comments_on_issue_spec.rb4
-rw-r--r--spec/features/issues/user_creates_issue_spec.rb17
-rw-r--r--spec/features/issues/user_edits_issue_spec.rb181
-rw-r--r--spec/features/issues/user_interacts_with_awards_spec.rb16
-rw-r--r--spec/features/issues/user_uses_quick_actions_spec.rb6
-rw-r--r--spec/features/jira_connect/branches_spec.rb4
-rw-r--r--spec/features/jira_oauth_provider_authorize_spec.rb42
-rw-r--r--spec/features/labels_hierarchy_spec.rb4
-rw-r--r--spec/features/markdown/keyboard_shortcuts_spec.rb4
-rw-r--r--spec/features/merge_request/maintainer_edits_fork_spec.rb5
-rw-r--r--spec/features/merge_request/merge_request_discussion_lock_spec.rb4
-rw-r--r--spec/features/merge_request/user_awards_emoji_spec.rb2
-rw-r--r--spec/features/merge_request/user_edits_assignees_sidebar_spec.rb207
-rw-r--r--spec/features/merge_request/user_locks_discussion_spec.rb2
-rw-r--r--spec/features/merge_request/user_merges_immediately_spec.rb2
-rw-r--r--spec/features/merge_request/user_merges_merge_request_spec.rb5
-rw-r--r--spec/features/merge_request/user_opens_checkout_branch_modal_spec.rb10
-rw-r--r--spec/features/merge_request/user_reverts_merge_request_spec.rb15
-rw-r--r--spec/features/merge_request/user_sees_check_out_branch_modal_spec.rb8
-rw-r--r--spec/features/merge_request/user_sees_merge_request_file_tree_sidebar_spec.rb56
-rw-r--r--spec/features/merge_request/user_sees_merge_request_pipelines_spec.rb14
-rw-r--r--spec/features/merge_request/user_sees_merge_widget_spec.rb5
-rw-r--r--spec/features/merge_request/user_sees_pipelines_spec.rb3
-rw-r--r--spec/features/merge_request/user_selects_branches_for_new_mr_spec.rb8
-rw-r--r--spec/features/merge_request/user_uses_quick_actions_spec.rb6
-rw-r--r--spec/features/merge_requests/user_filters_by_source_branch_spec.rb65
-rw-r--r--spec/features/monitor_sidebar_link_spec.rb102
-rw-r--r--spec/features/nav/new_nav_callout_spec.rb64
-rw-r--r--spec/features/nav/new_nav_for_everyone_callout_spec.rb55
-rw-r--r--spec/features/nav/new_nav_invite_members_spec.rb2
-rw-r--r--spec/features/nav/new_nav_toggle_spec.rb59
-rw-r--r--spec/features/nav/pinned_nav_items_spec.rb2
-rw-r--r--spec/features/nav/top_nav_responsive_spec.rb101
-rw-r--r--spec/features/nav/top_nav_spec.rb51
-rw-r--r--spec/features/nav/top_nav_tooltip_spec.rb25
-rw-r--r--spec/features/profiles/two_factor_auths_spec.rb2
-rw-r--r--spec/features/profiles/user_edit_profile_spec.rb94
-rw-r--r--spec/features/profiles/user_visits_profile_account_page_spec.rb17
-rw-r--r--spec/features/profiles/user_visits_profile_authentication_log_spec.rb14
-rw-r--r--spec/features/profiles/user_visits_profile_preferences_page_spec.rb8
-rw-r--r--spec/features/profiles/user_visits_profile_spec.rb10
-rw-r--r--spec/features/profiles/user_visits_profile_ssh_keys_page_spec.rb17
-rw-r--r--spec/features/project_variables_spec.rb36
-rw-r--r--spec/features/projects/active_tabs_spec.rb94
-rw-r--r--spec/features/projects/blobs/blob_show_spec.rb4
-rw-r--r--spec/features/projects/blobs/edit_spec.rb2
-rw-r--r--spec/features/projects/branches/user_creates_branch_spec.rb2
-rw-r--r--spec/features/projects/branches_spec.rb6
-rw-r--r--spec/features/projects/ci/editor_spec.rb8
-rw-r--r--spec/features/projects/clusters/gcp_spec.rb4
-rw-r--r--spec/features/projects/clusters/user_spec.rb4
-rw-r--r--spec/features/projects/clusters_spec.rb6
-rw-r--r--spec/features/projects/commit/comments/user_adds_comment_spec.rb2
-rw-r--r--spec/features/projects/commit/mini_pipeline_graph_spec.rb4
-rw-r--r--spec/features/projects/commit/user_sees_pipelines_tab_spec.rb3
-rw-r--r--spec/features/projects/compare_spec.rb9
-rw-r--r--spec/features/projects/confluence/user_views_confluence_page_spec.rb8
-rw-r--r--spec/features/projects/environments/environments_spec.rb20
-rw-r--r--spec/features/projects/features_visibility_spec.rb85
-rw-r--r--spec/features/projects/files/project_owner_creates_license_file_spec.rb4
-rw-r--r--spec/features/projects/files/user_edits_files_spec.rb19
-rw-r--r--spec/features/projects/files/user_find_file_spec.rb14
-rw-r--r--spec/features/projects/files/user_reads_pipeline_status_spec.rb2
-rw-r--r--spec/features/projects/files/user_searches_for_files_spec.rb6
-rw-r--r--spec/features/projects/forks/fork_list_spec.rb2
-rw-r--r--spec/features/projects/graph_spec.rb4
-rw-r--r--spec/features/projects/integrations/user_activates_issue_tracker_spec.rb6
-rw-r--r--spec/features/projects/integrations/user_activates_jira_spec.rb14
-rw-r--r--spec/features/projects/issuable_templates_spec.rb21
-rw-r--r--spec/features/projects/issues/email_participants_spec.rb14
-rw-r--r--spec/features/projects/jobs/user_browses_jobs_spec.rb8
-rw-r--r--spec/features/projects/jobs_spec.rb2
-rw-r--r--spec/features/projects/members/sorting_spec.rb2
-rw-r--r--spec/features/projects/members/user_requests_access_spec.rb11
-rw-r--r--spec/features/projects/milestones/user_interacts_with_labels_spec.rb6
-rw-r--r--spec/features/projects/navbar_spec.rb34
-rw-r--r--spec/features/projects/network_graph_spec.rb8
-rw-r--r--spec/features/projects/new_project_spec.rb91
-rw-r--r--spec/features/projects/pages/user_configures_pages_pipeline_spec.rb38
-rw-r--r--spec/features/projects/pages/user_edits_settings_spec.rb16
-rw-r--r--spec/features/projects/pipelines/pipeline_spec.rb60
-rw-r--r--spec/features/projects/pipelines/pipelines_spec.rb49
-rw-r--r--spec/features/projects/releases/user_creates_release_spec.rb3
-rw-r--r--spec/features/projects/releases/user_views_edit_release_spec.rb8
-rw-r--r--spec/features/projects/settings/monitor_settings_spec.rb11
-rw-r--r--spec/features/projects/settings/registry_settings_cleanup_tags_spec.rb11
-rw-r--r--spec/features/projects/settings/registry_settings_spec.rb11
-rw-r--r--spec/features/projects/show/user_sees_collaboration_links_spec.rb8
-rw-r--r--spec/features/projects/snippets/show_spec.rb59
-rw-r--r--spec/features/projects/tree/tree_show_spec.rb18
-rw-r--r--spec/features/projects/user_sees_sidebar_spec.rb88
-rw-r--r--spec/features/projects/user_uses_shortcuts_spec.rb59
-rw-r--r--spec/features/projects/wikis_spec.rb2
-rw-r--r--spec/features/projects/work_items/linked_work_items_spec.rb4
-rw-r--r--spec/features/projects/work_items/work_item_children_spec.rb4
-rw-r--r--spec/features/projects/work_items/work_item_spec.rb35
-rw-r--r--spec/features/projects_spec.rb26
-rw-r--r--spec/features/search/user_searches_for_code_spec.rb2
-rw-r--r--spec/features/search/user_searches_for_comments_spec.rb2
-rw-r--r--spec/features/search/user_searches_for_commits_spec.rb2
-rw-r--r--spec/features/search/user_searches_for_issues_spec.rb2
-rw-r--r--spec/features/search/user_searches_for_merge_requests_spec.rb2
-rw-r--r--spec/features/search/user_searches_for_milestones_spec.rb2
-rw-r--r--spec/features/search/user_searches_for_projects_spec.rb2
-rw-r--r--spec/features/search/user_searches_for_users_spec.rb6
-rw-r--r--spec/features/search/user_searches_for_wiki_pages_spec.rb2
-rw-r--r--spec/features/search/user_uses_header_search_field_spec.rb82
-rw-r--r--spec/features/snippets/search_snippets_spec.rb2
-rw-r--r--spec/features/snippets/show_spec.rb66
-rw-r--r--spec/features/snippets/user_creates_snippet_spec.rb2
-rw-r--r--spec/features/task_lists_spec.rb6
-rw-r--r--spec/features/unsubscribe_links_spec.rb8
-rw-r--r--spec/features/uploads/user_uploads_avatar_to_profile_spec.rb5
-rw-r--r--spec/features/usage_stats_consent_spec.rb29
-rw-r--r--spec/features/user_can_display_performance_bar_spec.rb2
-rw-r--r--spec/features/user_sees_active_nav_items_spec.rb63
-rw-r--r--spec/features/user_sees_revert_modal_spec.rb7
-rw-r--r--spec/features/users/active_sessions_spec.rb18
-rw-r--r--spec/features/users/anonymous_sessions_spec.rb8
-rw-r--r--spec/features/users/email_verification_on_login_spec.rb6
-rw-r--r--spec/features/users/login_spec.rb34
-rw-r--r--spec/features/users/logout_spec.rb2
-rw-r--r--spec/features/users/overview_spec.rb43
-rw-r--r--spec/features/users/rss_spec.rb55
-rw-r--r--spec/features/users/show_spec.rb54
-rw-r--r--spec/features/users/signup_spec.rb2
-rw-r--r--spec/features/users/snippets_spec.rb12
-rw-r--r--spec/features/users/terms_spec.rb8
-rw-r--r--spec/features/users/user_browses_projects_on_user_page_spec.rb8
-rw-r--r--spec/features/webauthn_spec.rb14
-rw-r--r--spec/features/whats_new_spec.rb50
-rw-r--r--spec/finders/ci/catalog/resources/versions_finder_spec.rb106
-rw-r--r--spec/finders/ci/runners_finder_spec.rb64
-rw-r--r--spec/finders/data_transfer/mocked_transfer_finder_spec.rb22
-rw-r--r--spec/finders/milestones_finder_spec.rb2
-rw-r--r--spec/finders/organizations/user_organizations_finder_spec.rb50
-rw-r--r--spec/finders/packages/group_packages_finder_spec.rb2
-rw-r--r--spec/finders/packages/pypi/packages_finder_spec.rb10
-rw-r--r--spec/finders/personal_access_tokens_finder_spec.rb44
-rw-r--r--spec/finders/projects/ml/model_finder_spec.rb57
-rw-r--r--spec/finders/projects_finder_spec.rb31
-rw-r--r--spec/finders/user_group_notification_settings_finder_spec.rb50
-rw-r--r--spec/finders/vs_code/settings/settings_finder_spec.rb8
-rw-r--r--spec/fixtures/api/schemas/entities/admin_users_data_attributes_paths.json62
-rw-r--r--spec/fixtures/api/schemas/entities/member.json34
-rw-r--r--spec/fixtures/api/schemas/entities/merge_request_noteable.json6
-rw-r--r--spec/fixtures/api/schemas/graphql/packages/package_details.json12
-rw-r--r--spec/fixtures/api/schemas/graphql/packages/package_pypi_metadata.json40
-rw-r--r--spec/fixtures/api/schemas/group_link/group_link.json5
-rw-r--r--spec/fixtures/api/schemas/job/test_report_summary.json34
-rw-r--r--spec/fixtures/api/schemas/ml/get_latest_versions.json80
-rw-r--r--spec/fixtures/api/schemas/ml/get_model.json51
-rw-r--r--spec/fixtures/api/schemas/ml/update_model.json51
-rw-r--r--spec/fixtures/click_house/migrations/drop_table/1_create_some_table.rb14
-rw-r--r--spec/fixtures/click_house/migrations/drop_table/2_drop_some_table.rb11
-rw-r--r--spec/fixtures/click_house/migrations/duplicate_name/1_create_some_table.rb14
-rw-r--r--spec/fixtures/click_house/migrations/duplicate_name/2_create_some_table.rb14
-rw-r--r--spec/fixtures/click_house/migrations/duplicate_version/1_create_some_table.rb14
-rw-r--r--spec/fixtures/click_house/migrations/duplicate_version/1_drop_some_table.rb11
-rw-r--r--spec/fixtures/click_house/migrations/migration_with_error/1_migration_with_error.rb9
-rw-r--r--spec/fixtures/click_house/migrations/migrations_over_multiple_databases/1_create_some_table_on_main_db.rb15
-rw-r--r--spec/fixtures/click_house/migrations/migrations_over_multiple_databases/2_create_some_table_on_another_db.rb16
-rw-r--r--spec/fixtures/click_house/migrations/migrations_over_multiple_databases/3_change_some_table_on_main_db.rb11
-rw-r--r--spec/fixtures/click_house/migrations/plain_table_creation/1_create_some_table.rb14
-rw-r--r--spec/fixtures/click_house/migrations/plain_table_creation_on_invalid_database/1_create_some_table.rb16
-rw-r--r--spec/fixtures/click_house/migrations/table_creation_with_down_method/1_create_some_table.rb20
-rw-r--r--spec/fixtures/origin_cert_key.pem0
-rw-r--r--spec/fixtures/security_reports/master/gl-common-scanning-report.json4
-rw-r--r--spec/fixtures/tooling/danger/rubocop_todo/cop1.yml5
-rw-r--r--spec/fixtures/tooling/danger/rubocop_todo/cop2.yml4
-rw-r--r--spec/frontend/__helpers__/dom_shims/clipboard_event.js1
-rw-r--r--spec/frontend/__helpers__/dom_shims/drag_event.js1
-rw-r--r--spec/frontend/__helpers__/dom_shims/index.js2
-rw-r--r--spec/frontend/__helpers__/emoji.js11
-rw-r--r--spec/frontend/__helpers__/local_storage_helper.js3
-rw-r--r--spec/frontend/__helpers__/mock_observability_client.js19
-rw-r--r--spec/frontend/__mocks__/@gitlab/ui.js2
-rw-r--r--spec/frontend/admin/abuse_report/components/abuse_report_app_spec.js30
-rw-r--r--spec/frontend/admin/abuse_report/components/abuse_report_notes_spec.js98
-rw-r--r--spec/frontend/admin/abuse_report/components/labels_select_spec.js2
-rw-r--r--spec/frontend/admin/abuse_report/components/notes/__snapshots__/abuse_report_note_body_spec.js.snap15
-rw-r--r--spec/frontend/admin/abuse_report/components/notes/abuse_report_discussion_spec.js79
-rw-r--r--spec/frontend/admin/abuse_report/components/notes/abuse_report_note_body_spec.js27
-rw-r--r--spec/frontend/admin/abuse_report/components/notes/abuse_report_note_spec.js80
-rw-r--r--spec/frontend/admin/abuse_report/components/report_details_spec.js2
-rw-r--r--spec/frontend/admin/abuse_report/mock_data.js212
-rw-r--r--spec/frontend/admin/users/components/app_spec.js85
-rw-r--r--spec/frontend/admin/users/components/users_table_spec.js141
-rw-r--r--spec/frontend/admin/users/constants.js4
-rw-r--r--spec/frontend/alert_spec.js4
-rw-r--r--spec/frontend/analytics/cycle_analytics/components/filter_bar_spec.js2
-rw-r--r--spec/frontend/analytics/cycle_analytics/mock_data.js6
-rw-r--r--spec/frontend/analytics/cycle_analytics/store/mutations_spec.js4
-rw-r--r--spec/frontend/analytics/product_analytics/components/activity_chart_spec.js34
-rw-r--r--spec/frontend/analytics/shared/components/metric_tile_spec.js21
-rw-r--r--spec/frontend/authentication/two_factor_auth/components/recovery_codes_spec.js2
-rw-r--r--spec/frontend/awards_handler_spec.js31
-rw-r--r--spec/frontend/batch_comments/components/submit_dropdown_spec.js110
-rw-r--r--spec/frontend/batch_comments/stores/modules/batch_comments/actions_spec.js2
-rw-r--r--spec/frontend/behaviors/gl_emoji_spec.js12
-rw-r--r--spec/frontend/behaviors/load_startup_css_spec.js48
-rw-r--r--spec/frontend/blob/components/blob_header_spec.js11
-rw-r--r--spec/frontend/blob/components/blob_header_viewer_switcher_spec.js33
-rw-r--r--spec/frontend/boards/board_card_inner_spec.js128
-rw-r--r--spec/frontend/boards/cache_updates_spec.js2
-rw-r--r--spec/frontend/boards/mock_data.js3
-rw-r--r--spec/frontend/boards/stores/actions_spec.js2
-rw-r--r--spec/frontend/ci/artifacts/components/job_artifacts_table_spec.js9
-rw-r--r--spec/frontend/ci/catalog/components/details/ci_resource_components_spec.js73
-rw-r--r--spec/frontend/ci/catalog/components/details/ci_resource_header_spec.js7
-rw-r--r--spec/frontend/ci/catalog/components/list/catalog_header_spec.js37
-rw-r--r--spec/frontend/ci/catalog/components/list/ci_resources_list_item_spec.js12
-rw-r--r--spec/frontend/ci/catalog/components/pages/ci_resources_page_spec.js211
-rw-r--r--spec/frontend/ci/catalog/global_catalog_spec.js17
-rw-r--r--spec/frontend/ci/catalog/index_spec.js48
-rw-r--r--spec/frontend/ci/catalog/mock.js53
-rw-r--r--spec/frontend/ci/ci_variable_list/components/ci_variable_drawer_spec.js13
-rw-r--r--spec/frontend/ci/ci_variable_list/components/ci_variable_modal_spec.js576
-rw-r--r--spec/frontend/ci/ci_variable_list/components/ci_variable_settings_spec.js101
-rw-r--r--spec/frontend/ci/common/pipelines_table_spec.js8
-rw-r--r--spec/frontend/ci/job_details/components/job_header_spec.js6
-rw-r--r--spec/frontend/ci/job_details/components/log/line_header_spec.js21
-rw-r--r--spec/frontend/ci/job_details/components/log/mock_data.js66
-rw-r--r--spec/frontend/ci/job_details/components/sidebar/stages_dropdown_spec.js5
-rw-r--r--spec/frontend/ci/job_details/job_app_spec.js2
-rw-r--r--spec/frontend/ci/job_details/store/actions_spec.js24
-rw-r--r--spec/frontend/ci/job_details/store/mutations_spec.js90
-rw-r--r--spec/frontend/ci/job_details/store/utils_spec.js18
-rw-r--r--spec/frontend/ci/jobs_page/components/job_cells/job_cell_spec.js10
-rw-r--r--spec/frontend/ci/jobs_page/components/jobs_table_spec.js6
-rw-r--r--spec/frontend/ci/pipeline_details/graph/components/graph_component_spec.js91
-rw-r--r--spec/frontend/ci/pipeline_details/graph/components/job_item_spec.js24
-rw-r--r--spec/frontend/ci/pipeline_details/graph/components/job_name_component_spec.js2
-rw-r--r--spec/frontend/ci/pipeline_details/graph/components/linked_pipeline_spec.js2
-rw-r--r--spec/frontend/ci/pipeline_details/header/pipeline_details_header_spec.js22
-rw-r--r--spec/frontend/ci/pipeline_editor/components/file-nav/branch_switcher_spec.js134
-rw-r--r--spec/frontend/ci/pipeline_mini_graph/legacy_pipeline_stage_spec.js4
-rw-r--r--spec/frontend/ci/pipeline_mini_graph/linked_pipelines_mini_list_spec.js4
-rw-r--r--spec/frontend/ci/pipeline_schedules/components/table/cells/pipeline_schedule_last_pipeline_spec.js12
-rw-r--r--spec/frontend/ci/pipelines_page/components/empty_state/ios_templates_spec.js133
-rw-r--r--spec/frontend/ci/pipelines_page/components/empty_state/no_ci_empty_state_spec.js35
-rw-r--r--spec/frontend/ci/pipelines_page/components/pipeline_labels_spec.js44
-rw-r--r--spec/frontend/ci/pipelines_page/components/pipeline_triggerer_spec.js13
-rw-r--r--spec/frontend/ci/pipelines_page/pipelines_spec.js38
-rw-r--r--spec/frontend/ci/runner/admin_new_runner_app/admin_new_runner_app_spec.js6
-rw-r--r--spec/frontend/ci/runner/admin_runners/admin_runners_app_spec.js5
-rw-r--r--spec/frontend/ci/runner/components/cells/runner_summary_cell_spec.js54
-rw-r--r--spec/frontend/ci/runner/components/registration/registration_dropdown_spec.js2
-rw-r--r--spec/frontend/ci/runner/components/registration/registration_feedback_banner_spec.js52
-rw-r--r--spec/frontend/ci/runner/components/registration/registration_token_spec.js2
-rw-r--r--spec/frontend/ci/runner/components/runner_created_at_spec.js97
-rw-r--r--spec/frontend/ci/runner/components/runner_filtered_search_bar_spec.js13
-rw-r--r--spec/frontend/ci/runner/components/runner_header_spec.js20
-rw-r--r--spec/frontend/ci/runner/components/runner_list_header_spec.js31
-rw-r--r--spec/frontend/ci/runner/components/runner_update_form_spec.js10
-rw-r--r--spec/frontend/ci/runner/group_new_runner_app/group_new_runner_app_spec.js6
-rw-r--r--spec/frontend/ci/runner/mock_data.js17
-rw-r--r--spec/frontend/ci/runner/project_new_runner_app/project_new_runner_app_spec.js6
-rw-r--r--spec/frontend/ci/runner/runner_edit/runner_edit_app_spec.js2
-rw-r--r--spec/frontend/ci/runner/sentry_utils_spec.js4
-rw-r--r--spec/frontend/clusters_list/components/clusters_spec.js4
-rw-r--r--spec/frontend/clusters_list/store/actions_spec.js2
-rw-r--r--spec/frontend/commit/components/commit_box_pipeline_status_spec.js12
-rw-r--r--spec/frontend/content_editor/extensions/code_block_highlight_spec.js12
-rw-r--r--spec/frontend/content_editor/extensions/copy_paste_spec.js112
-rw-r--r--spec/frontend/content_editor/extensions/horizontal_rule_spec.js2
-rw-r--r--spec/frontend/content_editor/extensions/html_marks_spec.js89
-rw-r--r--spec/frontend/content_editor/extensions/word_break_spec.js8
-rw-r--r--spec/frontend/contribution_events/components/contribution_event/contribution_event_base_spec.js2
-rw-r--r--spec/frontend/crm/organization_form_wrapper_spec.js8
-rw-r--r--spec/frontend/custom_emoji/components/delete_item_spec.js4
-rw-r--r--spec/frontend/design_management/components/design_notes/__snapshots__/design_note_signed_out_spec.js.snap4
-rw-r--r--spec/frontend/design_management/components/design_notes/__snapshots__/design_reply_form_spec.js.snap4
-rw-r--r--spec/frontend/design_management/components/design_notes/design_note_spec.js45
-rw-r--r--spec/frontend/design_management/components/design_overlay_spec.js2
-rw-r--r--spec/frontend/design_management/components/design_presentation_spec.js2
-rw-r--r--spec/frontend/design_management/components/list/__snapshots__/item_spec.js.snap12
-rw-r--r--spec/frontend/diffs/components/app_spec.js97
-rw-r--r--spec/frontend/diffs/components/diff_file_header_spec.js2
-rw-r--r--spec/frontend/diffs/components/diff_file_spec.js4
-rw-r--r--spec/frontend/diffs/components/shared/__snapshots__/findings_drawer_spec.js.snap281
-rw-r--r--spec/frontend/diffs/components/shared/findings_drawer_item_spec.js54
-rw-r--r--spec/frontend/diffs/components/shared/findings_drawer_spec.js35
-rw-r--r--spec/frontend/diffs/mock_data/findings_drawer.js33
-rw-r--r--spec/frontend/diffs/store/actions_spec.js4
-rw-r--r--spec/frontend/diffs/store/utils_spec.js2
-rw-r--r--spec/frontend/diffs/utils/sort_findings_by_file_spec.js (renamed from spec/frontend/diffs/utils/sort_errors_by_file_spec.js)14
-rw-r--r--spec/frontend/emoji/awards_app/store/actions_spec.js4
-rw-r--r--spec/frontend/emoji/index_spec.js266
-rw-r--r--spec/frontend/environments/environment_form_spec.js1
-rw-r--r--spec/frontend/environments/environments_app_spec.js47
-rw-r--r--spec/frontend/environments/graphql/mock_data.js10
-rw-r--r--spec/frontend/environments/graphql/resolvers/flux_spec.js309
-rw-r--r--spec/frontend/environments/graphql/resolvers/kubernetes_spec.js139
-rw-r--r--spec/frontend/environments/kubernetes_overview_spec.js2
-rw-r--r--spec/frontend/environments/kubernetes_status_bar_spec.js92
-rw-r--r--spec/frontend/environments/kubernetes_summary_spec.js6
-rw-r--r--spec/frontend/error_tracking/components/error_details_spec.js8
-rw-r--r--spec/frontend/fixtures/issues.rb2
-rw-r--r--spec/frontend/fixtures/merge_requests.rb8
-rw-r--r--spec/frontend/fixtures/snippet.rb2
-rw-r--r--spec/frontend/fixtures/startup_css.rb89
-rw-r--r--spec/frontend/groups/components/overview_tabs_spec.js4
-rw-r--r--spec/frontend/groups/members/utils_spec.js14
-rw-r--r--spec/frontend/header_spec.js9
-rw-r--r--spec/frontend/helpers/help_page_helper_spec.js1
-rw-r--r--spec/frontend/helpers/init_simple_app_helper_spec.js37
-rw-r--r--spec/frontend/ide/components/commit_sidebar/new_merge_request_option_spec.js28
-rw-r--r--spec/frontend/ide/components/jobs/detail/description_spec.js7
-rw-r--r--spec/frontend/ide/components/jobs/item_spec.js2
-rw-r--r--spec/frontend/ide/components/repo_editor_spec.js9
-rw-r--r--spec/frontend/import/details/components/bulk_import_details_app_spec.js18
-rw-r--r--spec/frontend/import/details/components/import_details_app_spec.js2
-rw-r--r--spec/frontend/import/details/components/import_details_table_spec.js66
-rw-r--r--spec/frontend/import_entities/import_groups/components/import_status_spec.js99
-rw-r--r--spec/frontend/import_entities/import_projects/components/github_organizations_box_spec.js4
-rw-r--r--spec/frontend/incidents/components/incidents_list_spec.js2
-rw-r--r--spec/frontend/integrations/edit/components/dynamic_field_spec.js35
-rw-r--r--spec/frontend/integrations/edit/components/integration_form_spec.js4
-rw-r--r--spec/frontend/integrations/overrides/components/integration_overrides_spec.js2
-rw-r--r--spec/frontend/invite_members/components/invite_members_modal_spec.js60
-rw-r--r--spec/frontend/invite_members/components/members_token_select_spec.js15
-rw-r--r--spec/frontend/invite_members/utils/member_utils_spec.js16
-rw-r--r--spec/frontend/issuable/components/locked_badge_spec.js2
-rw-r--r--spec/frontend/issuable/components/status_badge_spec.js8
-rw-r--r--spec/frontend/issues/dashboard/components/issues_dashboard_app_spec.js4
-rw-r--r--spec/frontend/issues/issue_spec.js12
-rw-r--r--spec/frontend/issues/list/components/issues_list_app_spec.js4
-rw-r--r--spec/frontend/issues/service_desk/components/service_desk_list_app_spec.js4
-rw-r--r--spec/frontend/issues/show/components/header_actions_spec.js52
-rw-r--r--spec/frontend/issues/show/components/issue_header_spec.js6
-rw-r--r--spec/frontend/issues/show/components/sticky_header_spec.js12
-rw-r--r--spec/frontend/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/setup_instructions_spec.js44
-rw-r--r--spec/frontend/lib/utils/color_utils_spec.js10
-rw-r--r--spec/frontend/lib/utils/common_utils_spec.js24
-rw-r--r--spec/frontend/lib/utils/datetime/timeago_utility_spec.js19
-rw-r--r--spec/frontend/lib/utils/forms_spec.js28
-rw-r--r--spec/frontend/members/components/avatars/group_avatar_spec.js25
-rw-r--r--spec/frontend/members/components/icons/private_icon_spec.js30
-rw-r--r--spec/frontend/members/components/table/member_source_spec.js15
-rw-r--r--spec/frontend/members/components/table/members_table_spec.js19
-rw-r--r--spec/frontend/members/components/table/role_dropdown_spec.js12
-rw-r--r--spec/frontend/members/mock_data.js16
-rw-r--r--spec/frontend/members/store/actions_spec.js10
-rw-r--r--spec/frontend/members/utils_spec.js45
-rw-r--r--spec/frontend/merge_requests/components/sticky_header_spec.js48
-rw-r--r--spec/frontend/milestones/components/milestone_combobox_spec.js228
-rw-r--r--spec/frontend/milestones/stores/mutations_spec.js24
-rw-r--r--spec/frontend/ml/model_registry/apps/index_ml_models_spec.js (renamed from spec/frontend/ml/model_registry/routes/models/index/components/ml_models_index_spec.js)36
-rw-r--r--spec/frontend/ml/model_registry/apps/show_ml_model_spec.js75
-rw-r--r--spec/frontend/ml/model_registry/apps/show_ml_model_version_spec.js15
-rw-r--r--spec/frontend/ml/model_registry/components/model_row_spec.js45
-rw-r--r--spec/frontend/ml/model_registry/components/search_bar_spec.js86
-rw-r--r--spec/frontend/ml/model_registry/mock_data.js49
-rw-r--r--spec/frontend/ml/model_registry/routes/models/index/components/model_row_spec.js42
-rw-r--r--spec/frontend/notes/components/comment_field_layout_spec.js26
-rw-r--r--spec/frontend/observability/client_spec.js221
-rw-r--r--spec/frontend/observability/loader_spec.js (renamed from spec/frontend/observability/skeleton_spec.js)67
-rw-r--r--spec/frontend/observability/observability_container_spec.js128
-rw-r--r--spec/frontend/observability/observability_empty_state_spec.js36
-rw-r--r--spec/frontend/observability/provisioned_observability_container_spec.js156
-rw-r--r--spec/frontend/organizations/new/components/app_spec.js86
-rw-r--r--spec/frontend/organizations/settings/general/components/app_spec.js19
-rw-r--r--spec/frontend/organizations/settings/general/components/organization_settings_spec.js126
-rw-r--r--spec/frontend/organizations/shared/components/new_edit_form_spec.js118
-rw-r--r--spec/frontend/organizations/users/components/app_spec.js81
-rw-r--r--spec/frontend/organizations/users/mock_data.js22
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/pypi_installation_spec.js.snap2
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/additional_metadata_spec.js2
-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/package_history_spec.js2
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/package_versions_list_spec.js2
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/list/__snapshots__/publish_method_spec.js.snap2
-rw-r--r--spec/frontend/packages_and_registries/package_registry/mock_data.js2
-rw-r--r--spec/frontend/packages_and_registries/settings/group/components/package_settings_spec.js56
-rw-r--r--spec/frontend/packages_and_registries/settings/group/mock_data.js2
-rw-r--r--spec/frontend/pages/admin/application_settings/appearances/preview_sign_in/index_spec.js10
-rw-r--r--spec/frontend/pages/groups/sso/index_spec.js10
-rw-r--r--spec/frontend/pages/import/bulk_imports/details/index_spec.js37
-rw-r--r--spec/frontend/pages/passwords/new/index_spec.js10
-rw-r--r--spec/frontend/pages/projects/forks/new/components/fork_form_spec.js42
-rw-r--r--spec/frontend/pages/projects/pipeline_schedules/shared/components/interval_pattern_input_spec.js2
-rw-r--r--spec/frontend/pages/registrations/new/index_spec.js10
-rw-r--r--spec/frontend/pages/sessions/new/index_spec.js10
-rw-r--r--spec/frontend/pipeline_wizard/templates/pages_spec.js6
-rw-r--r--spec/frontend/projects/members/utils_spec.js14
-rw-r--r--spec/frontend/projects/pipelines/charts/components/__snapshots__/statistics_list_spec.js.snap6
-rw-r--r--spec/frontend/projects/pipelines/charts/mock_data.js6
-rw-r--r--spec/frontend/projects/settings_service_desk/components/custom_email_form_spec.js34
-rw-r--r--spec/frontend/projects/settings_service_desk/components/service_desk_root_spec.js5
-rw-r--r--spec/frontend/projects/settings_service_desk/components/service_desk_setting_spec.js36
-rw-r--r--spec/frontend/protected_branches/protected_branch_edit_spec.js348
-rw-r--r--spec/frontend/repository/components/__snapshots__/last_commit_spec.js.snap6
-rw-r--r--spec/frontend/repository/components/blob_content_viewer_spec.js34
-rw-r--r--spec/frontend/repository/components/commit_info_spec.js21
-rw-r--r--spec/frontend/repository/components/table/__snapshots__/row_spec.js.snap18
-rw-r--r--spec/frontend/search/sidebar/components/app_spec.js8
-rw-r--r--spec/frontend/search/sidebar/components/blobs_filters_spec.js39
-rw-r--r--spec/frontend/search/sidebar/components/issues_filters_spec.js34
-rw-r--r--spec/frontend/search/sidebar/components/label_filter_spec.js67
-rw-r--r--spec/frontend/search/sidebar/components/merge_requests_filters_spec.js36
-rw-r--r--spec/frontend/search/store/getters_spec.js19
-rw-r--r--spec/frontend/security_configuration/components/training_provider_list_spec.js2
-rw-r--r--spec/frontend/sentry/init_sentry_spec.js4
-rw-r--r--spec/frontend/sentry/sentry_browser_wrapper_spec.js29
-rw-r--r--spec/frontend/shortcuts_spec.js40
-rw-r--r--spec/frontend/sidebar/components/lock/__snapshots__/edit_form_spec.js.snap8
-rw-r--r--spec/frontend/sidebar/components/lock/issuable_lock_form_spec.js4
-rw-r--r--spec/frontend/sidebar/components/move/issuable_move_dropdown_spec.js358
-rw-r--r--spec/frontend/sidebar/components/sidebar_dropdown_widget_spec.js2
-rw-r--r--spec/frontend/silent_mode_settings/components/app_spec.js13
-rw-r--r--spec/frontend/snippets/components/snippet_header_spec.js16
-rw-r--r--spec/frontend/super_sidebar/components/global_search/components/global_search_default_places_spec.js13
-rw-r--r--spec/frontend/super_sidebar/components/global_search/components/global_search_spec.js43
-rw-r--r--spec/frontend/super_sidebar/components/nav_item_spec.js13
-rw-r--r--spec/frontend/super_sidebar/components/user_bar_spec.js1
-rw-r--r--spec/frontend/super_sidebar/components/user_menu_spec.js13
-rw-r--r--spec/frontend/super_sidebar/mock_data.js1
-rw-r--r--spec/frontend/super_sidebar/utils_spec.js4
-rw-r--r--spec/frontend/terraform/components/init_command_modal_spec.js12
-rw-r--r--spec/frontend/time_tracking/components/timelogs_app_spec.js4
-rw-r--r--spec/frontend/token_access/token_projects_table_spec.js21
-rw-r--r--spec/frontend/tracking/dispatch_snowplow_event_spec.js4
-rw-r--r--spec/frontend/tracking/tracking_initialization_spec.js2
-rw-r--r--spec/frontend/users/profile/components/report_abuse_button_spec.js79
-rw-r--r--spec/frontend/vue_merge_request_widget/components/approvals/approvals_spec.js60
-rw-r--r--spec/frontend/vue_merge_request_widget/components/checks/conflicts_spec.js13
-rw-r--r--spec/frontend/vue_merge_request_widget/components/checks/message_spec.js16
-rw-r--r--spec/frontend/vue_merge_request_widget/components/checks/rebase_spec.js323
-rw-r--r--spec/frontend/vue_merge_request_widget/components/checks/unresolved_discussions_spec.js49
-rw-r--r--spec/frontend/vue_merge_request_widget/components/merge_checks_spec.js103
-rw-r--r--spec/frontend/vue_merge_request_widget/components/states/mr_widget_ready_to_merge_spec.js12
-rw-r--r--spec/frontend/vue_merge_request_widget/components/widget/app_spec.js10
-rw-r--r--spec/frontend/vue_merge_request_widget/components/widget/dynamic_content_spec.js19
-rw-r--r--spec/frontend/vue_merge_request_widget/components/widget/widget_spec.js2
-rw-r--r--spec/frontend/vue_merge_request_widget/extensions/security_reports/mr_widget_security_reports_spec.js63
-rw-r--r--spec/frontend/vue_merge_request_widget/extentions/accessibility/index_spec.js12
-rw-r--r--spec/frontend/vue_merge_request_widget/mr_widget_options_spec.js6
-rw-r--r--spec/frontend/vue_shared/components/ci_badge_link_spec.js158
-rw-r--r--spec/frontend/vue_shared/components/ci_icon_spec.js143
-rw-r--r--spec/frontend/vue_shared/components/diff_stats_dropdown_spec.js52
-rw-r--r--spec/frontend/vue_shared/components/ensure_data_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/entity_select/entity_select_spec.js27
-rw-r--r--spec/frontend/vue_shared/components/entity_select/organization_select_spec.js179
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/filtered_search_bar_root_spec.js21
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/base_token_spec.js52
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/user_token_spec.js111
-rw-r--r--spec/frontend/vue_shared/components/form/errors_alert_spec.js60
-rw-r--r--spec/frontend/vue_shared/components/form/input_copy_toggle_visibility_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/list_selector/group_item_spec.js55
-rw-r--r--spec/frontend/vue_shared/components/list_selector/index_spec.js257
-rw-r--r--spec/frontend/vue_shared/components/list_selector/mock_data.js41
-rw-r--r--spec/frontend/vue_shared/components/list_selector/user_item_spec.js55
-rw-r--r--spec/frontend/vue_shared/components/markdown/markdown_editor_spec.js25
-rw-r--r--spec/frontend/vue_shared/components/notes/__snapshots__/noteable_warning_spec.js.snap6
-rw-r--r--spec/frontend/vue_shared/components/notes/noteable_warning_spec.js18
-rw-r--r--spec/frontend/vue_shared/components/notes/system_note_spec.js10
-rw-r--r--spec/frontend/vue_shared/components/source_viewer/components/blame_info_spec.js25
-rw-r--r--spec/frontend/vue_shared/components/source_viewer/components/chunk_new_spec.js15
-rw-r--r--spec/frontend/vue_shared/components/source_viewer/mock_data.js78
-rw-r--r--spec/frontend/vue_shared/components/source_viewer/source_viewer_new_spec.js104
-rw-r--r--spec/frontend/vue_shared/components/source_viewer/utils_spec.js14
-rw-r--r--spec/frontend/vue_shared/components/users_table/mock_data.js23
-rw-r--r--spec/frontend/vue_shared/components/users_table/user_avatar_spec.js (renamed from spec/frontend/admin/users/components/user_avatar_spec.js)46
-rw-r--r--spec/frontend/vue_shared/components/users_table/users_table_spec.js95
-rw-r--r--spec/frontend/vue_shared/components/web_ide_link_spec.js10
-rw-r--r--spec/frontend/vue_shared/issuable/show/components/issuable_body_spec.js1
-rw-r--r--spec/frontend/vue_shared/issuable/show/components/issuable_show_root_spec.js2
-rw-r--r--spec/frontend/vue_shared/issuable/show/components/issuable_title_spec.js43
-rw-r--r--spec/frontend/vue_shared/mixins/gl_feature_flags_mixin_spec.js2
-rw-r--r--spec/frontend/webhooks/components/__snapshots__/push_events_spec.js.snap76
-rw-r--r--spec/frontend/work_items/components/notes/system_note_spec.js10
-rw-r--r--spec/frontend/work_items/components/notes/work_item_comment_form_spec.js4
-rw-r--r--spec/frontend/work_items/components/notes/work_item_note_actions_spec.js4
-rw-r--r--spec/frontend/work_items/components/shared/work_item_link_child_contents_spec.js60
-rw-r--r--spec/frontend/work_items/components/shared/work_item_links_menu_spec.js30
-rw-r--r--spec/frontend/work_items/components/shared/work_item_token_input_spec.js119
-rw-r--r--spec/frontend/work_items/components/work_item_actions_spec.js29
-rw-r--r--spec/frontend/work_items/components/work_item_award_emoji_spec.js27
-rw-r--r--spec/frontend/work_items/components/work_item_description_spec.js2
-rw-r--r--spec/frontend/work_items/components/work_item_detail_spec.js40
-rw-r--r--spec/frontend/work_items/components/work_item_labels_spec.js33
-rw-r--r--spec/frontend/work_items/components/work_item_links/work_item_link_child_spec.js1
-rw-r--r--spec/frontend/work_items/components/work_item_links/work_item_links_spec.js19
-rw-r--r--spec/frontend/work_items/components/work_item_links/work_item_tree_spec.js19
-rw-r--r--spec/frontend/work_items/components/work_item_milestone_spec.js53
-rw-r--r--spec/frontend/work_items/components/work_item_parent_spec.js84
-rw-r--r--spec/frontend/work_items/components/work_item_relationships/__snapshots__/work_item_relationship_list_spec.js.snap1
-rw-r--r--spec/frontend/work_items/components/work_item_relationships/work_item_add_relationship_form_spec.js3
-rw-r--r--spec/frontend/work_items/components/work_item_relationships/work_item_relationships_spec.js33
-rw-r--r--spec/frontend/work_items/components/work_item_state_toggle_button_spec.js4
-rw-r--r--spec/frontend/work_items/components/work_item_title_spec.js20
-rw-r--r--spec/frontend/work_items/components/work_item_todos_spec.js2
-rw-r--r--spec/frontend/work_items/list/components/work_items_list_app_spec.js4
-rw-r--r--spec/frontend/work_items/mock_data.js121
-rw-r--r--spec/frontend_integration/snippets/snippets_notes_spec.js2
-rw-r--r--spec/frontend_integration/test_helpers/mock_server/routes/emojis.js2
-rw-r--r--spec/frontend_integration/test_helpers/mock_server/routes/graphql.js6
-rw-r--r--spec/graphql/mutations/base_mutation_spec.rb58
-rw-r--r--spec/graphql/mutations/ci/runner/delete_spec.rb2
-rw-r--r--spec/graphql/mutations/ci/runner/update_spec.rb2
-rw-r--r--spec/graphql/mutations/container_repositories/destroy_tags_spec.rb4
-rw-r--r--spec/graphql/mutations/merge_requests/update_spec.rb14
-rw-r--r--spec/graphql/mutations/namespace/package_settings/update_spec.rb12
-rw-r--r--spec/graphql/mutations/saved_replies/create_spec.rb34
-rw-r--r--spec/graphql/mutations/saved_replies/destroy_spec.rb26
-rw-r--r--spec/graphql/mutations/saved_replies/update_spec.rb34
-rw-r--r--spec/graphql/resolvers/ci/catalog/resource_resolver_spec.rb123
-rw-r--r--spec/graphql/resolvers/ci/catalog/resources_resolver_spec.rb71
-rw-r--r--spec/graphql/resolvers/ci/catalog/versions_resolver_spec.rb66
-rw-r--r--spec/graphql/resolvers/ci/runners_resolver_spec.rb28
-rw-r--r--spec/graphql/resolvers/container_repository_tags_resolver_spec.rb136
-rw-r--r--spec/graphql/resolvers/data_transfer/group_data_transfer_resolver_spec.rb20
-rw-r--r--spec/graphql/resolvers/data_transfer/project_data_transfer_resolver_spec.rb22
-rw-r--r--spec/graphql/resolvers/projects_resolver_spec.rb2
-rw-r--r--spec/graphql/resolvers/saved_reply_resolver_spec.rb28
-rw-r--r--spec/graphql/resolvers/users/frecent_groups_resolver_spec.rb7
-rw-r--r--spec/graphql/resolvers/users/frecent_projects_resolver_spec.rb7
-rw-r--r--spec/graphql/types/analytics/cycle_analytics/value_stream_type_spec.rb11
-rw-r--r--spec/graphql/types/base_argument_spec.rb39
-rw-r--r--spec/graphql/types/ci/catalog/resource_sort_enum_spec.rb13
-rw-r--r--spec/graphql/types/ci/catalog/resource_type_spec.rb28
-rw-r--r--spec/graphql/types/container_registry/protection/rule_access_level_enum_spec.rb9
-rw-r--r--spec/graphql/types/container_registry/protection/rule_type_spec.rb35
-rw-r--r--spec/graphql/types/data_transfer/project_data_transfer_type_spec.rb22
-rw-r--r--spec/graphql/types/merge_request_review_state_enum_spec.rb8
-rw-r--r--spec/graphql/types/notes/noteable_interface_spec.rb1
-rw-r--r--spec/graphql/types/organizations/organization_type_spec.rb2
-rw-r--r--spec/graphql/types/organizations/organization_user_badge_type_spec.rb10
-rw-r--r--spec/graphql/types/packages/package_base_type_spec.rb3
-rw-r--r--spec/graphql/types/packages/package_details_type_spec.rb6
-rw-r--r--spec/graphql/types/packages/package_type_spec.rb5
-rw-r--r--spec/graphql/types/packages/protection/rule_type_spec.rb6
-rw-r--r--spec/graphql/types/packages/pypi/metadatum_type_spec.rb9
-rw-r--r--spec/graphql/types/permission_types/abuse_report_spec.rb15
-rw-r--r--spec/graphql/types/permission_types/ci/job_spec.rb2
-rw-r--r--spec/graphql/types/permission_types/ci/pipeline_spec.rb13
-rw-r--r--spec/graphql/types/permission_types/package_spec.rb11
-rw-r--r--spec/graphql/types/project_type_spec.rb91
-rw-r--r--spec/graphql/types/projects/detailed_import_status_type_spec.rb23
-rw-r--r--spec/graphql/types/security/codequality_reports_comparer/report_generation_status_enum_spec.rb11
-rw-r--r--spec/graphql/types/security/codequality_reports_comparer/status_enum_spec.rb4
-rw-r--r--spec/graphql/types/security/codequality_reports_comparer_type_spec.rb2
-rw-r--r--spec/graphql/types/user_type_spec.rb1
-rw-r--r--spec/graphql/types/work_items/linked_item_type_spec.rb6
-rw-r--r--spec/helpers/admin/components_helper_spec.rb1
-rw-r--r--spec/helpers/admin/user_actions_helper_spec.rb59
-rw-r--r--spec/helpers/application_helper_spec.rb23
-rw-r--r--spec/helpers/auth_helper_spec.rb103
-rw-r--r--spec/helpers/blob_helper_spec.rb2
-rw-r--r--spec/helpers/ci/catalog/resources_helper_spec.rb27
-rw-r--r--spec/helpers/ci/jobs_helper_spec.rb1
-rw-r--r--spec/helpers/ci/pipeline_editor_helper_spec.rb2
-rw-r--r--spec/helpers/ci/pipelines_helper_spec.rb71
-rw-r--r--spec/helpers/ci/status_helper_spec.rb107
-rw-r--r--spec/helpers/colors_helper_spec.rb47
-rw-r--r--spec/helpers/environment_helper_spec.rb39
-rw-r--r--spec/helpers/environments_helper_spec.rb17
-rw-r--r--spec/helpers/events_helper_spec.rb112
-rw-r--r--spec/helpers/groups/group_members_helper_spec.rb12
-rw-r--r--spec/helpers/groups_helper_spec.rb4
-rw-r--r--spec/helpers/ide_helper_spec.rb2
-rw-r--r--spec/helpers/issuables_helper_spec.rb20
-rw-r--r--spec/helpers/members_helper_spec.rb15
-rw-r--r--spec/helpers/nav/new_dropdown_helper_spec.rb35
-rw-r--r--spec/helpers/nav_helper_spec.rb32
-rw-r--r--spec/helpers/operations_helper_spec.rb2
-rw-r--r--spec/helpers/organizations/organization_helper_spec.rb36
-rw-r--r--spec/helpers/preferences_helper_spec.rb10
-rw-r--r--spec/helpers/projects/pipeline_helper_spec.rb1
-rw-r--r--spec/helpers/projects_helper_spec.rb18
-rw-r--r--spec/helpers/search_helper_spec.rb24
-rw-r--r--spec/helpers/sidebars_helper_spec.rb13
-rw-r--r--spec/helpers/sorting_helper_spec.rb2
-rw-r--r--spec/helpers/users/callouts_helper_spec.rb22
-rw-r--r--spec/helpers/users_helper_spec.rb2
-rw-r--r--spec/helpers/visibility_level_helper_spec.rb30
-rw-r--r--spec/helpers/vite_helper_spec.rb59
-rw-r--r--spec/helpers/wiki_helper_spec.rb11
-rw-r--r--spec/initializers/action_cable_subscription_adapter_identifier_spec.rb3
-rw-r--r--spec/initializers/active_record_transaction_observer_spec.rb2
-rw-r--r--spec/initializers/diagnostic_reports_spec.rb2
-rw-r--r--spec/initializers/google_cloud_profiler_spec.rb2
-rw-r--r--spec/initializers/memory_watchdog_spec.rb2
-rw-r--r--spec/lib/api/entities/bulk_imports/entity_failure_spec.rb8
-rw-r--r--spec/lib/api/entities/wiki_page_spec.rb6
-rw-r--r--spec/lib/api/github/entities_spec.rb31
-rw-r--r--spec/lib/api/helpers/rate_limiter_spec.rb8
-rw-r--r--spec/lib/api/helpers_spec.rb75
-rw-r--r--spec/lib/atlassian/jira_connect/client_spec.rb87
-rw-r--r--spec/lib/backup/gitaly_backup_spec.rb41
-rw-r--r--spec/lib/banzai/filter/asset_proxy_filter_spec.rb101
-rw-r--r--spec/lib/banzai/filter/math_filter_spec.rb23
-rw-r--r--spec/lib/banzai/filter/references/user_reference_filter_spec.rb5
-rw-r--r--spec/lib/banzai/reference_parser/user_parser_spec.rb10
-rw-r--r--spec/lib/bitbucket/representation/pull_request_comment_spec.rb8
-rw-r--r--spec/lib/bitbucket/representation/repo_spec.rb7
-rw-r--r--spec/lib/bulk_imports/clients/graphql_spec.rb2
-rw-r--r--spec/lib/bulk_imports/common/pipelines/badges_pipeline_spec.rb12
-rw-r--r--spec/lib/bulk_imports/common/pipelines/entity_finisher_spec.rb5
-rw-r--r--spec/lib/bulk_imports/pipeline/runner_spec.rb161
-rw-r--r--spec/lib/bulk_imports/pipeline_schema_info_spec.rb60
-rw-r--r--spec/lib/bulk_imports/projects/pipelines/container_expiration_policy_pipeline_spec.rb4
-rw-r--r--spec/lib/bulk_imports/projects/pipelines/external_pull_requests_pipeline_spec.rb4
-rw-r--r--spec/lib/bulk_imports/projects/pipelines/merge_requests_pipeline_spec.rb11
-rw-r--r--spec/lib/bulk_imports/projects/pipelines/releases_pipeline_spec.rb2
-rw-r--r--spec/lib/bulk_imports/source_url_builder_spec.rb78
-rw-r--r--spec/lib/click_house/models/audit_event_spec.rb132
-rw-r--r--spec/lib/click_house/models/base_model_spec.rb117
-rw-r--r--spec/lib/container_registry/client_spec.rb14
-rw-r--r--spec/lib/container_registry/gitlab_api_client_spec.rb2
-rw-r--r--spec/lib/container_registry/tag_spec.rb83
-rw-r--r--spec/lib/generators/batched_background_migration/batched_background_migration_generator_spec.rb1
-rw-r--r--spec/lib/generators/batched_background_migration/expected_files/queue_my_batched_migration.txt4
-rw-r--r--spec/lib/generators/gitlab/snowplow_event_definition_generator_spec.rb95
-rw-r--r--spec/lib/generators/gitlab/usage_metric_definition/redis_hll_generator_spec.rb6
-rw-r--r--spec/lib/generators/gitlab/usage_metric_definition_generator_spec.rb21
-rw-r--r--spec/lib/generators/model/mocks/migration_file.txt3
-rw-r--r--spec/lib/generators/model/model_generator_spec.rb4
-rw-r--r--spec/lib/gitlab/alert_management/payload/base_spec.rb12
-rw-r--r--spec/lib/gitlab/analytics/cycle_analytics/average_spec.rb12
-rw-r--r--spec/lib/gitlab/asset_proxy_spec.rb2
-rw-r--r--spec/lib/gitlab/auth/ldap/auth_hash_spec.rb2
-rw-r--r--spec/lib/gitlab/auth/ldap/config_spec.rb10
-rw-r--r--spec/lib/gitlab/auth/ldap/person_spec.rb6
-rw-r--r--spec/lib/gitlab/auth/o_auth/user_spec.rb8
-rw-r--r--spec/lib/gitlab/auth/saml/auth_hash_spec.rb6
-rw-r--r--spec/lib/gitlab/auth/saml/config_spec.rb39
-rw-r--r--spec/lib/gitlab/auth/saml/user_spec.rb20
-rw-r--r--spec/lib/gitlab/auth_spec.rb145
-rw-r--r--spec/lib/gitlab/background_migration/backfill_packages_tags_project_id_spec.rb42
-rw-r--r--spec/lib/gitlab/background_migration/batched_migration_job_spec.rb103
-rw-r--r--spec/lib/gitlab/background_migration/copy_column_using_background_migration_job_spec.rb22
-rw-r--r--spec/lib/gitlab/background_migration/delete_invalid_protected_branch_merge_access_levels_spec.rb76
-rw-r--r--spec/lib/gitlab/background_migration/delete_invalid_protected_branch_push_access_levels_spec.rb76
-rw-r--r--spec/lib/gitlab/background_migration/delete_invalid_protected_tag_create_access_levels_spec.rb76
-rw-r--r--spec/lib/gitlab/background_migration/delete_orphaned_operational_vulnerabilities_spec.rb16
-rw-r--r--spec/lib/gitlab/background_migration/delete_orphans_approval_merge_request_rules_spec.rb9
-rw-r--r--spec/lib/gitlab/background_migration/delete_orphans_approval_project_rules_spec.rb9
-rw-r--r--spec/lib/gitlab/background_migration/destroy_invalid_group_members_spec.rb24
-rw-r--r--spec/lib/gitlab/background_migration/destroy_invalid_members_spec.rb84
-rw-r--r--spec/lib/gitlab/background_migration/destroy_invalid_project_members_spec.rb56
-rw-r--r--spec/lib/gitlab/background_migration/disable_legacy_open_source_licence_for_recent_public_projects_spec.rb19
-rw-r--r--spec/lib/gitlab/background_migration/disable_legacy_open_source_license_for_inactive_public_projects_spec.rb31
-rw-r--r--spec/lib/gitlab/background_migration/disable_legacy_open_source_license_for_no_issues_no_repo_projects_spec.rb37
-rw-r--r--spec/lib/gitlab/background_migration/disable_legacy_open_source_license_for_one_member_no_repo_projects_spec.rb37
-rw-r--r--spec/lib/gitlab/background_migration/disable_legacy_open_source_license_for_projects_less_than_five_mb_spec.rb33
-rw-r--r--spec/lib/gitlab/background_migration/disable_legacy_open_source_license_for_projects_less_than_one_mb_spec.rb33
-rw-r--r--spec/lib/gitlab/background_migration/expire_o_auth_tokens_spec.rb17
-rw-r--r--spec/lib/gitlab/background_migration/fix_incoherent_packages_size_on_project_statistics_spec.rb10
-rw-r--r--spec/lib/gitlab/background_migration/legacy_upload_mover_spec.rb56
-rw-r--r--spec/lib/gitlab/background_migration/remove_backfilled_job_artifacts_expire_at_spec.rb2
-rw-r--r--spec/lib/gitlab/batch_worker_context_spec.rb6
-rw-r--r--spec/lib/gitlab/bitbucket_import/importer_spec.rb2
-rw-r--r--spec/lib/gitlab/bitbucket_import/importers/issue_importer_spec.rb8
-rw-r--r--spec/lib/gitlab/bitbucket_import/importers/issues_importer_spec.rb47
-rw-r--r--spec/lib/gitlab/bitbucket_import/importers/issues_notes_importer_spec.rb6
-rw-r--r--spec/lib/gitlab/bitbucket_import/importers/pull_request_importer_spec.rb8
-rw-r--r--spec/lib/gitlab/bitbucket_import/importers/pull_request_notes_importer_spec.rb214
-rw-r--r--spec/lib/gitlab/bitbucket_import/importers/pull_requests_importer_spec.rb4
-rw-r--r--spec/lib/gitlab/bitbucket_import/importers/pull_requests_notes_importer_spec.rb4
-rw-r--r--spec/lib/gitlab/bitbucket_import/importers/repository_importer_spec.rb17
-rw-r--r--spec/lib/gitlab/bullet/exclusions_spec.rb2
-rw-r--r--spec/lib/gitlab/cache_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/ansi2html_spec.rb8
-rw-r--r--spec/lib/gitlab/ci/ansi2json/line_spec.rb27
-rw-r--r--spec/lib/gitlab/ci/ansi2json/state_spec.rb21
-rw-r--r--spec/lib/gitlab/ci/ansi2json_spec.rb28
-rw-r--r--spec/lib/gitlab/ci/badge/pipeline/status_spec.rb5
-rw-r--r--spec/lib/gitlab/ci/build/artifacts/metadata/entry_spec.rb20
-rw-r--r--spec/lib/gitlab/ci/build/context/build_spec.rb20
-rw-r--r--spec/lib/gitlab/ci/build/hook_spec.rb6
-rw-r--r--spec/lib/gitlab/ci/build/policy/changes_spec.rb53
-rw-r--r--spec/lib/gitlab/ci/build/policy/variables_spec.rb37
-rw-r--r--spec/lib/gitlab/ci/build/rules/rule/clause/changes_spec.rb7
-rw-r--r--spec/lib/gitlab/ci/config/entry/bridge_spec.rb60
-rw-r--r--spec/lib/gitlab/ci/config/entry/commands_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/config/entry/environment_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/config/entry/image_spec.rb6
-rw-r--r--spec/lib/gitlab/ci/config/entry/job_spec.rb76
-rw-r--r--spec/lib/gitlab/ci/config/entry/pages_spec.rb49
-rw-r--r--spec/lib/gitlab/ci/config/entry/policy_spec.rb3
-rw-r--r--spec/lib/gitlab/ci/config/entry/processable_spec.rb14
-rw-r--r--spec/lib/gitlab/ci/config/entry/root_spec.rb14
-rw-r--r--spec/lib/gitlab/ci/config/entry/service_spec.rb12
-rw-r--r--spec/lib/gitlab/ci/config/extendable/entry_spec.rb13
-rw-r--r--spec/lib/gitlab/ci/config/external/file/base_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/config/external/mapper_spec.rb12
-rw-r--r--spec/lib/gitlab/ci/config/header/input_spec.rb31
-rw-r--r--spec/lib/gitlab/ci/config/interpolation/inputs_spec.rb86
-rw-r--r--spec/lib/gitlab/ci/jwt_v2_spec.rb36
-rw-r--r--spec/lib/gitlab/ci/parsers/sbom/cyclonedx_properties_spec.rb36
-rw-r--r--spec/lib/gitlab/ci/parsers/sbom/source/container_scanning_spec.rb58
-rw-r--r--spec/lib/gitlab/ci/parsers/security/common_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/parsers/security/secret_detection_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/pipeline/chain/template_usage_spec.rb4
-rw-r--r--spec/lib/gitlab/ci/pipeline/expression/lexeme/equals_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/pipeline/seed/build_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/reports/accessibility_reports_comparer_spec.rb4
-rw-r--r--spec/lib/gitlab/ci/reports/accessibility_reports_spec.rb4
-rw-r--r--spec/lib/gitlab/ci/reports/test_suite_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/status/composite_spec.rb59
-rw-r--r--spec/lib/gitlab/ci/status/stage/factory_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/status/waiting_for_callback_spec.rb37
-rw-r--r--spec/lib/gitlab/ci/tags/bulk_insert_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/templates/Jobs/deploy_gitlab_ci_yaml_spec.rb6
-rw-r--r--spec/lib/gitlab/ci/templates/Jobs/sast_iac_latest_gitlab_ci_yaml_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/templates/auto_devops_gitlab_ci_yaml_spec.rb42
-rw-r--r--spec/lib/gitlab/ci/variables/collection/item_spec.rb8
-rw-r--r--spec/lib/gitlab/ci/yaml_processor/dag_spec.rb24
-rw-r--r--spec/lib/gitlab/ci/yaml_processor_spec.rb137
-rw-r--r--spec/lib/gitlab/composer/version_index_spec.rb26
-rw-r--r--spec/lib/gitlab/config/entry/factory_spec.rb12
-rw-r--r--spec/lib/gitlab/config/entry/validators_spec.rb2
-rw-r--r--spec/lib/gitlab/config_checker/puma_rugged_checker_spec.rb65
-rw-r--r--spec/lib/gitlab/conflict/file_spec.rb4
-rw-r--r--spec/lib/gitlab/data_builder/build_spec.rb2
-rw-r--r--spec/lib/gitlab/data_builder/pipeline_spec.rb2
-rw-r--r--spec/lib/gitlab/data_builder/push_spec.rb4
-rw-r--r--spec/lib/gitlab/database/background_migration/batched_job_spec.rb2
-rw-r--r--spec/lib/gitlab/database/background_migration/batched_job_transition_log_spec.rb4
-rw-r--r--spec/lib/gitlab/database/background_migration/batched_migration_spec.rb11
-rw-r--r--spec/lib/gitlab/database/background_migration/batched_migration_wrapper_spec.rb22
-rw-r--r--spec/lib/gitlab/database/background_migration/prometheus_metrics_spec.rb13
-rw-r--r--spec/lib/gitlab/database/bulk_update_spec.rb2
-rw-r--r--spec/lib/gitlab/database/dictionary_spec.rb84
-rw-r--r--spec/lib/gitlab/database/dynamic_model_helpers_spec.rb67
-rw-r--r--spec/lib/gitlab/database/gitlab_schema_spec.rb4
-rw-r--r--spec/lib/gitlab/database/loose_foreign_keys_spec.rb16
-rw-r--r--spec/lib/gitlab/database/migration_helpers/cascading_namespace_settings_spec.rb2
-rw-r--r--spec/lib/gitlab/database/migration_helpers/convert_to_bigint_spec.rb133
-rw-r--r--spec/lib/gitlab/database/migration_helpers/wraparound_autovacuum_spec.rb30
-rw-r--r--spec/lib/gitlab/database/migration_helpers_spec.rb40
-rw-r--r--spec/lib/gitlab/database/migrations/batched_background_migration_helpers_spec.rb6
-rw-r--r--spec/lib/gitlab/database/migrations/constraints_helpers_spec.rb40
-rw-r--r--spec/lib/gitlab/database/migrations/instrumentation_spec.rb16
-rw-r--r--spec/lib/gitlab/database/migrations/milestone_mixin_spec.rb13
-rw-r--r--spec/lib/gitlab/database/migrations/reestablished_connection_stack_spec.rb2
-rw-r--r--spec/lib/gitlab/database/migrations/test_batched_background_runner_spec.rb104
-rw-r--r--spec/lib/gitlab/database/no_cross_db_foreign_keys_spec.rb8
-rw-r--r--spec/lib/gitlab/database/no_new_tables_with_gitlab_main_schema_spec.rb63
-rw-r--r--spec/lib/gitlab/database/partitioning/detached_partition_dropper_spec.rb64
-rw-r--r--spec/lib/gitlab/database/partitioning/monthly_strategy_spec.rb36
-rw-r--r--spec/lib/gitlab/database/partitioning/partition_manager_spec.rb85
-rw-r--r--spec/lib/gitlab/database/partitioning/sliding_list_strategy_spec.rb27
-rw-r--r--spec/lib/gitlab/database/partitioning_migration_helpers/index_helpers_spec.rb7
-rw-r--r--spec/lib/gitlab/database/partitioning_migration_helpers/table_management_helpers_spec.rb541
-rw-r--r--spec/lib/gitlab/database/postgres_constraint_spec.rb16
-rw-r--r--spec/lib/gitlab/database/postgres_index_spec.rb4
-rw-r--r--spec/lib/gitlab/database/query_analyzers/gitlab_schemas_metrics_spec.rb6
-rw-r--r--spec/lib/gitlab/database/query_analyzers/gitlab_schemas_validate_connection_spec.rb16
-rw-r--r--spec/lib/gitlab/database/query_analyzers/prevent_set_operator_mismatch/columns_spec.rb88
-rw-r--r--spec/lib/gitlab/database/query_analyzers/prevent_set_operator_mismatch/common_table_expressions_spec.rb97
-rw-r--r--spec/lib/gitlab/database/query_analyzers/prevent_set_operator_mismatch/froms_spec.rb76
-rw-r--r--spec/lib/gitlab/database/query_analyzers/prevent_set_operator_mismatch/node_spec.rb68
-rw-r--r--spec/lib/gitlab/database/query_analyzers/prevent_set_operator_mismatch/references_spec.rb41
-rw-r--r--spec/lib/gitlab/database/query_analyzers/prevent_set_operator_mismatch/select_stmt_spec.rb361
-rw-r--r--spec/lib/gitlab/database/query_analyzers/prevent_set_operator_mismatch/targets_spec.rb94
-rw-r--r--spec/lib/gitlab/database/query_analyzers/prevent_set_operator_mismatch_spec.rb84
-rw-r--r--spec/lib/gitlab/database/reindexing_spec.rb2
-rw-r--r--spec/lib/gitlab/database/tables_locker_spec.rb44
-rw-r--r--spec/lib/gitlab/database/tables_truncate_spec.rb6
-rw-r--r--spec/lib/gitlab/database/transaction/observer_spec.rb2
-rw-r--r--spec/lib/gitlab/dependency_linker/base_linker_spec.rb4
-rw-r--r--spec/lib/gitlab/dependency_linker/cargo_toml_linker_spec.rb2
-rw-r--r--spec/lib/gitlab/dependency_linker/cartfile_linker_spec.rb2
-rw-r--r--spec/lib/gitlab/dependency_linker/composer_json_linker_spec.rb2
-rw-r--r--spec/lib/gitlab/dependency_linker/gemfile_linker_spec.rb2
-rw-r--r--spec/lib/gitlab/dependency_linker/gemspec_linker_spec.rb2
-rw-r--r--spec/lib/gitlab/dependency_linker/go_mod_linker_spec.rb2
-rw-r--r--spec/lib/gitlab/dependency_linker/go_sum_linker_spec.rb2
-rw-r--r--spec/lib/gitlab/dependency_linker/godeps_json_linker_spec.rb2
-rw-r--r--spec/lib/gitlab/dependency_linker/package_json_linker_spec.rb2
-rw-r--r--spec/lib/gitlab/dependency_linker/podfile_linker_spec.rb2
-rw-r--r--spec/lib/gitlab/dependency_linker/podspec_json_linker_spec.rb2
-rw-r--r--spec/lib/gitlab/dependency_linker/podspec_linker_spec.rb2
-rw-r--r--spec/lib/gitlab/dependency_linker/requirements_txt_linker_spec.rb2
-rw-r--r--spec/lib/gitlab/diff/file_collection/compare_spec.rb8
-rw-r--r--spec/lib/gitlab/diff/file_collection/merge_request_diff_batch_spec.rb25
-rw-r--r--spec/lib/gitlab/diff/file_collection/paginated_merge_request_diff_spec.rb16
-rw-r--r--spec/lib/gitlab/diff/file_spec.rb5
-rw-r--r--spec/lib/gitlab/diff/formatters/file_formatter_spec.rb2
-rw-r--r--spec/lib/gitlab/diff/highlight_cache_spec.rb20
-rw-r--r--spec/lib/gitlab/diff/highlight_spec.rb10
-rw-r--r--spec/lib/gitlab/diff/inline_diff_marker_spec.rb6
-rw-r--r--spec/lib/gitlab/diff/line_spec.rb14
-rw-r--r--spec/lib/gitlab/diff/suggestion_diff_spec.rb13
-rw-r--r--spec/lib/gitlab/diff/suggestion_spec.rb22
-rw-r--r--spec/lib/gitlab/diff/suggestions_parser_spec.rb67
-rw-r--r--spec/lib/gitlab/discussions_diff/file_collection_spec.rb15
-rw-r--r--spec/lib/gitlab/email/handler/service_desk_handler_spec.rb92
-rw-r--r--spec/lib/gitlab/email/handler_spec.rb8
-rw-r--r--spec/lib/gitlab/email/receiver_spec.rb2
-rw-r--r--spec/lib/gitlab/encoding_helper_spec.rb2
-rw-r--r--spec/lib/gitlab/endpoint_attributes_spec.rb14
-rw-r--r--spec/lib/gitlab/error_tracking/stack_trace_highlight_decorator_spec.rb2
-rw-r--r--spec/lib/gitlab/external_authorization/client_spec.rb2
-rw-r--r--spec/lib/gitlab/favicon_spec.rb4
-rw-r--r--spec/lib/gitlab/feature_categories_spec.rb2
-rw-r--r--spec/lib/gitlab/file_detector_spec.rb18
-rw-r--r--spec/lib/gitlab/gfm/reference_rewriter_spec.rb10
-rw-r--r--spec/lib/gitlab/git/blame_spec.rb8
-rw-r--r--spec/lib/gitlab/git/blob_spec.rb12
-rw-r--r--spec/lib/gitlab/git/commit_spec.rb24
-rw-r--r--spec/lib/gitlab/git/merge_base_spec.rb8
-rw-r--r--spec/lib/gitlab/git/repository_spec.rb10
-rw-r--r--spec/lib/gitlab/git/rugged_impl/use_rugged_spec.rb116
-rw-r--r--spec/lib/gitlab/git/tree_spec.rb118
-rw-r--r--spec/lib/gitlab/git_access_spec.rb2
-rw-r--r--spec/lib/gitlab/git_audit_event_spec.rb79
-rw-r--r--spec/lib/gitlab/gitaly_client/commit_service_spec.rb29
-rw-r--r--spec/lib/gitlab/gitaly_client/conflict_files_stitcher_spec.rb52
-rw-r--r--spec/lib/gitlab/gitaly_client/operation_service_spec.rb4
-rw-r--r--spec/lib/gitlab/gitaly_client/ref_service_spec.rb4
-rw-r--r--spec/lib/gitlab/gitaly_client/repository_service_spec.rb38
-rw-r--r--spec/lib/gitlab/gitaly_client/storage_settings_spec.rb18
-rw-r--r--spec/lib/gitlab/gitaly_client_spec.rb38
-rw-r--r--spec/lib/gitlab/github_import/attachments_downloader_spec.rb17
-rw-r--r--spec/lib/gitlab/github_import/client_spec.rb39
-rw-r--r--spec/lib/gitlab/github_import/importer/collaborators_importer_spec.rb2
-rw-r--r--spec/lib/gitlab/github_import/importer/diff_notes_importer_spec.rb2
-rw-r--r--spec/lib/gitlab/github_import/importer/issue_events_importer_spec.rb2
-rw-r--r--spec/lib/gitlab/github_import/importer/issues_importer_spec.rb2
-rw-r--r--spec/lib/gitlab/github_import/importer/lfs_objects_importer_spec.rb2
-rw-r--r--spec/lib/gitlab/github_import/importer/note_importer_spec.rb2
-rw-r--r--spec/lib/gitlab/github_import/importer/notes_importer_spec.rb2
-rw-r--r--spec/lib/gitlab/github_import/importer/protected_branches_importer_spec.rb2
-rw-r--r--spec/lib/gitlab/github_import/importer/pull_requests/review_requests_importer_spec.rb10
-rw-r--r--spec/lib/gitlab/github_import/importer/pull_requests_importer_spec.rb2
-rw-r--r--spec/lib/gitlab/github_import/issuable_finder_spec.rb64
-rw-r--r--spec/lib/gitlab/github_import/label_finder_spec.rb49
-rw-r--r--spec/lib/gitlab/github_import/milestone_finder_spec.rb57
-rw-r--r--spec/lib/gitlab/github_import/object_counter_spec.rb10
-rw-r--r--spec/lib/gitlab/github_import/parallel_scheduling_spec.rb12
-rw-r--r--spec/lib/gitlab/github_import/representation/to_hash_spec.rb12
-rw-r--r--spec/lib/gitlab/graphql/known_operations_spec.rb4
-rw-r--r--spec/lib/gitlab/graphql/tracers/metrics_tracer_spec.rb2
-rw-r--r--spec/lib/gitlab/group_search_results_spec.rb2
-rw-r--r--spec/lib/gitlab/hashed_path_spec.rb2
-rw-r--r--spec/lib/gitlab/health_checks/gitaly_check_spec.rb5
-rw-r--r--spec/lib/gitlab/highlight_spec.rb2
-rw-r--r--spec/lib/gitlab/i18n/translation_entry_spec.rb6
-rw-r--r--spec/lib/gitlab/import/import_failure_service_spec.rb38
-rw-r--r--spec/lib/gitlab/import_export/after_export_strategies/base_after_export_strategy_spec.rb2
-rw-r--r--spec/lib/gitlab/import_export/after_export_strategies/web_upload_strategy_spec.rb2
-rw-r--r--spec/lib/gitlab/import_export/all_models.yml2
-rw-r--r--spec/lib/gitlab/import_export/attribute_cleaner_spec.rb2
-rw-r--r--spec/lib/gitlab/import_export/attributes_permitter_spec.rb2
-rw-r--r--spec/lib/gitlab/import_export/command_line_util_spec.rb18
-rw-r--r--spec/lib/gitlab/import_export/error_spec.rb10
-rw-r--r--spec/lib/gitlab/import_export/fast_hash_serializer_spec.rb2
-rw-r--r--spec/lib/gitlab/import_export/json/ndjson_writer_spec.rb2
-rw-r--r--spec/lib/gitlab/import_export/lfs_restorer_spec.rb2
-rw-r--r--spec/lib/gitlab/import_export/lfs_saver_spec.rb2
-rw-r--r--spec/lib/gitlab/import_export/project/tree_saver_spec.rb2
-rw-r--r--spec/lib/gitlab/import_export/saver_spec.rb2
-rw-r--r--spec/lib/gitlab/import_export/snippet_repo_restorer_spec.rb2
-rw-r--r--spec/lib/gitlab/import_sources_spec.rb8
-rw-r--r--spec/lib/gitlab/instrumentation/redis_cluster_validator_spec.rb38
-rw-r--r--spec/lib/gitlab/instrumentation_helper_spec.rb61
-rw-r--r--spec/lib/gitlab/issues/rebalancing/state_spec.rb18
-rw-r--r--spec/lib/gitlab/jira/middleware_spec.rb40
-rw-r--r--spec/lib/gitlab/jira_import/handle_labels_service_spec.rb6
-rw-r--r--spec/lib/gitlab/jira_import/issue_serializer_spec.rb4
-rw-r--r--spec/lib/gitlab/jira_import/labels_importer_spec.rb10
-rw-r--r--spec/lib/gitlab/job_waiter_spec.rb66
-rw-r--r--spec/lib/gitlab/kubernetes/kubectl_cmd_spec.rb4
-rw-r--r--spec/lib/gitlab/kubernetes/role_spec.rb6
-rw-r--r--spec/lib/gitlab/language_data_spec.rb2
-rw-r--r--spec/lib/gitlab/mail_room/mail_room_spec.rb2
-rw-r--r--spec/lib/gitlab/markup_helper_spec.rb8
-rw-r--r--spec/lib/gitlab/memory/instrumentation_spec.rb2
-rw-r--r--spec/lib/gitlab/memory/reporter_spec.rb2
-rw-r--r--spec/lib/gitlab/memory/reports/heap_dump_spec.rb2
-rw-r--r--spec/lib/gitlab/memory/watchdog/configurator_spec.rb2
-rw-r--r--spec/lib/gitlab/memory/watchdog/event_reporter_spec.rb2
-rw-r--r--spec/lib/gitlab/memory/watchdog/handlers/null_handler_spec.rb2
-rw-r--r--spec/lib/gitlab/memory/watchdog/handlers/puma_handler_spec.rb2
-rw-r--r--spec/lib/gitlab/memory/watchdog/handlers/sidekiq_handler_spec.rb2
-rw-r--r--spec/lib/gitlab/memory/watchdog/monitor/rss_memory_limit_spec.rb2
-rw-r--r--spec/lib/gitlab/memory/watchdog/sidekiq_event_reporter_spec.rb2
-rw-r--r--spec/lib/gitlab/memory/watchdog_spec.rb2
-rw-r--r--spec/lib/gitlab/merge_requests/mergeability/check_result_spec.rb24
-rw-r--r--spec/lib/gitlab/metrics/exporter/base_exporter_spec.rb2
-rw-r--r--spec/lib/gitlab/metrics/rails_slis_spec.rb2
-rw-r--r--spec/lib/gitlab/metrics/samplers/threads_sampler_spec.rb8
-rw-r--r--spec/lib/gitlab/metrics/subscribers/action_cable_spec.rb2
-rw-r--r--spec/lib/gitlab/middleware/go_spec.rb4
-rw-r--r--spec/lib/gitlab/middleware/multipart_spec.rb4
-rw-r--r--spec/lib/gitlab/middleware/path_traversal_check_spec.rb183
-rw-r--r--spec/lib/gitlab/omniauth_initializer_spec.rb119
-rw-r--r--spec/lib/gitlab/other_markup_spec.rb43
-rw-r--r--spec/lib/gitlab/pages/deployment_update_spec.rb2
-rw-r--r--spec/lib/gitlab/pages/virtual_host_finder_spec.rb35
-rw-r--r--spec/lib/gitlab/pagination/cursor_based_keyset_spec.rb20
-rw-r--r--spec/lib/gitlab/pagination/keyset/order_spec.rb2
-rw-r--r--spec/lib/gitlab/pagination/offset_header_builder_spec.rb8
-rw-r--r--spec/lib/gitlab/patch/sidekiq_scheduled_enq_spec.rb26
-rw-r--r--spec/lib/gitlab/path_regex_spec.rb14
-rw-r--r--spec/lib/gitlab/popen_spec.rb8
-rw-r--r--spec/lib/gitlab/process_management_spec.rb4
-rw-r--r--spec/lib/gitlab/process_supervisor_spec.rb12
-rw-r--r--spec/lib/gitlab/project_template_spec.rb2
-rw-r--r--spec/lib/gitlab/quick_actions/extractor_spec.rb2
-rw-r--r--spec/lib/gitlab/redis/multi_store_spec.rb421
-rw-r--r--spec/lib/gitlab/redis/pubsub_spec.rb8
-rw-r--r--spec/lib/gitlab/reference_extractor_spec.rb4
-rw-r--r--spec/lib/gitlab/regex_requires_app_spec.rb40
-rw-r--r--spec/lib/gitlab/repository_cache_adapter_spec.rb12
-rw-r--r--spec/lib/gitlab/repository_hash_cache_spec.rb10
-rw-r--r--spec/lib/gitlab/repository_set_cache_spec.rb4
-rw-r--r--spec/lib/gitlab/rugged_instrumentation_spec.rb27
-rw-r--r--spec/lib/gitlab/runtime_spec.rb2
-rw-r--r--spec/lib/gitlab/search/abuse_detection_spec.rb4
-rw-r--r--spec/lib/gitlab/search_results_spec.rb6
-rw-r--r--spec/lib/gitlab/security/scan_configuration_spec.rb4
-rw-r--r--spec/lib/gitlab/seeders/ci/catalog/resource_seeder_spec.rb101
-rw-r--r--spec/lib/gitlab/shard_health_cache_spec.rb4
-rw-r--r--spec/lib/gitlab/sidekiq_config/cli_methods_spec.rb2
-rw-r--r--spec/lib/gitlab/sidekiq_config/worker_matcher_spec.rb76
-rw-r--r--spec/lib/gitlab/sidekiq_logging/structured_logger_spec.rb8
-rw-r--r--spec/lib/gitlab/sidekiq_middleware/duplicate_jobs/duplicate_job_spec.rb16
-rw-r--r--spec/lib/gitlab/sidekiq_middleware/server_metrics_spec.rb31
-rw-r--r--spec/lib/gitlab/sidekiq_middleware/skip_jobs_spec.rb15
-rw-r--r--spec/lib/gitlab/sidekiq_middleware/worker_context/client_spec.rb10
-rw-r--r--spec/lib/gitlab/sidekiq_status_spec.rb22
-rw-r--r--spec/lib/gitlab/ssh_public_key_spec.rb36
-rw-r--r--spec/lib/gitlab/string_range_marker_spec.rb8
-rw-r--r--spec/lib/gitlab/string_regex_marker_spec.rb18
-rw-r--r--spec/lib/gitlab/suggestions/suggestion_set_spec.rb2
-rw-r--r--spec/lib/gitlab/task_helpers_spec.rb6
-rw-r--r--spec/lib/gitlab/tracking/event_definition_spec.rb8
-rw-r--r--spec/lib/gitlab/url_builder_spec.rb1
-rw-r--r--spec/lib/gitlab/url_sanitizer_spec.rb8
-rw-r--r--spec/lib/gitlab/usage/metric_definition_spec.rb166
-rw-r--r--spec/lib/gitlab/usage/metric_spec.rb4
-rw-r--r--spec/lib/gitlab/usage/metrics/instrumentations/aggregated_metric_spec.rb3
-rw-r--r--spec/lib/gitlab/usage/metrics/instrumentations/count_projects_with_jira_dvcs_integration_metric_spec.rb50
-rw-r--r--spec/lib/gitlab/usage/metrics/instrumentations/database_metric_spec.rb2
-rw-r--r--spec/lib/gitlab/usage/metrics/instrumentations/generic_metric_spec.rb2
-rw-r--r--spec/lib/gitlab/usage/metrics/instrumentations/numbers_metric_spec.rb2
-rw-r--r--spec/lib/gitlab/usage/metrics/instrumentations/redis_hll_metric_spec.rb3
-rw-r--r--spec/lib/gitlab/usage/metrics/instrumentations/redis_metric_spec.rb3
-rw-r--r--spec/lib/gitlab/usage/service_ping/instrumented_payload_spec.rb4
-rw-r--r--spec/lib/gitlab/usage_data_counters/hll_redis_counter_spec.rb88
-rw-r--r--spec/lib/gitlab/usage_data_counters/redis_counter_spec.rb48
-rw-r--r--spec/lib/gitlab/usage_data_spec.rb8
-rw-r--r--spec/lib/gitlab/utils/log_limited_array_spec.rb2
-rw-r--r--spec/lib/gitlab/webpack/graphql_known_operations_spec.rb2
-rw-r--r--spec/lib/gitlab/wiki_pages/front_matter_parser_spec.rb2
-rw-r--r--spec/lib/gitlab/workhorse_spec.rb9
-rw-r--r--spec/lib/object_storage/config_spec.rb2
-rw-r--r--spec/lib/object_storage/direct_upload_spec.rb2
-rw-r--r--spec/lib/peek/views/rugged_spec.rb42
-rw-r--r--spec/lib/result_spec.rb2
-rw-r--r--spec/lib/rouge/formatters/html_gitlab_spec.rb10
-rw-r--r--spec/lib/safe_zip/entry_spec.rb8
-rw-r--r--spec/lib/safe_zip/extract_params_spec.rb4
-rw-r--r--spec/lib/safe_zip/extract_spec.rb10
-rw-r--r--spec/lib/sbom/purl_type/converter_spec.rb1
-rw-r--r--spec/lib/security/ci_configuration/container_scanning_build_action_spec.rb8
-rw-r--r--spec/lib/security/ci_configuration/sast_build_action_spec.rb14
-rw-r--r--spec/lib/security/ci_configuration/sast_iac_build_action_spec.rb8
-rw-r--r--spec/lib/security/ci_configuration/secret_detection_build_action_spec.rb8
-rw-r--r--spec/lib/sidebars/explore/menus/catalog_menu_spec.rb40
-rw-r--r--spec/lib/sidebars/menu_spec.rb6
-rw-r--r--spec/lib/sidebars/organizations/menus/manage_menu_spec.rb6
-rw-r--r--spec/lib/sidebars/projects/menus/scope_menu_spec.rb2
-rw-r--r--spec/lib/sidebars/projects/menus/security_compliance_menu_spec.rb3
-rw-r--r--spec/lib/sidebars/projects/menus/settings_menu_spec.rb12
-rw-r--r--spec/lib/sidebars/projects/super_sidebar_menus/monitor_menu_spec.rb1
-rw-r--r--spec/lib/sidebars/user_settings/menus/comment_templates_menu_spec.rb28
-rw-r--r--spec/lib/system_check/orphans/namespace_check_spec.rb14
-rw-r--r--spec/lib/system_check/orphans/repository_check_spec.rb22
-rw-r--r--spec/lib/system_check/sidekiq_check_spec.rb2
-rw-r--r--spec/lib/unnested_in_filters/dsl_spec.rb2
-rw-r--r--spec/lib/unnested_in_filters/rewriter_spec.rb12
-rw-r--r--spec/mailers/emails/pages_domains_spec.rb4
-rw-r--r--spec/metrics_server/metrics_server_spec.rb6
-rw-r--r--spec/migrations/20230929155123_migrate_disable_merge_trains_value_spec.rb83
-rw-r--r--spec/migrations/20231003045342_migrate_sidekiq_namespaced_jobs_spec.rb67
-rw-r--r--spec/migrations/20231016001000_fix_design_user_mentions_design_id_note_id_index_for_self_managed_spec.rb114
-rw-r--r--spec/migrations/20231016173129_queue_delete_invalid_protected_branch_merge_access_levels_spec.rb26
-rw-r--r--spec/migrations/20231016194927_queue_delete_invalid_protected_branch_push_access_levels_spec.rb26
-rw-r--r--spec/migrations/20231016194943_queue_delete_invalid_protected_tag_create_access_levels_spec.rb26
-rw-r--r--spec/migrations/20231019003052_swap_columns_for_ci_pipelines_pipeline_id_bigint_v2_spec.rb37
-rw-r--r--spec/migrations/20231019084731_swap_columns_for_ci_stages_pipeline_id_bigint_v2_spec.rb37
-rw-r--r--spec/migrations/20231019145202_add_status_to_packages_npm_metadata_caches_spec.rb22
-rw-r--r--spec/migrations/20231019223224_backfill_catalog_resources_name_and_description_spec.rb29
-rw-r--r--spec/migrations/20231020181652_add_index_packages_npm_metadata_caches_on_id_and_project_id_and_status_spec.rb24
-rw-r--r--spec/migrations/20231030071209_queue_backfill_packages_tags_project_id_spec.rb26
-rw-r--r--spec/migrations/20231102142554_migrate_zoekt_shards_to_zoekt_nodes_spec.rb44
-rw-r--r--spec/migrations/20231103223224_backfill_zoekt_node_id_on_indexed_namespaces_spec.rb79
-rw-r--r--spec/migrations/db/migrate/20231103162825_add_wolfi_purl_type_to_package_metadata_purl_types_spec.rb35
-rw-r--r--spec/migrations/schedule_fixing_security_scan_statuses_spec.rb11
-rw-r--r--spec/models/activity_pub/releases_subscription_spec.rb79
-rw-r--r--spec/models/ai/service_access_token_spec.rb21
-rw-r--r--spec/models/alert_management/http_integration_spec.rb6
-rw-r--r--spec/models/analytics/cycle_analytics/value_stream_spec.rb19
-rw-r--r--spec/models/appearance_spec.rb2
-rw-r--r--spec/models/application_setting_spec.rb40
-rw-r--r--spec/models/authentication_event_spec.rb4
-rw-r--r--spec/models/blob_viewer/base_spec.rb4
-rw-r--r--spec/models/bulk_imports/entity_spec.rb18
-rw-r--r--spec/models/bulk_imports/failure_spec.rb16
-rw-r--r--spec/models/ci/build_dependencies_spec.rb6
-rw-r--r--spec/models/ci/build_spec.rb76
-rw-r--r--spec/models/ci/build_trace_chunks/redis_spec.rb221
-rw-r--r--spec/models/ci/build_trace_chunks/redis_trace_chunks_spec.rb12
-rw-r--r--spec/models/ci/catalog/components_project_spec.rb49
-rw-r--r--spec/models/ci/catalog/listing_spec.rb196
-rw-r--r--spec/models/ci/catalog/resource_spec.rb141
-rw-r--r--spec/models/ci/catalog/resources/component_spec.rb17
-rw-r--r--spec/models/ci/catalog/resources/version_spec.rb91
-rw-r--r--spec/models/ci/job_artifact_spec.rb2
-rw-r--r--spec/models/ci/job_token/scope_spec.rb49
-rw-r--r--spec/models/ci/pipeline_spec.rb172
-rw-r--r--spec/models/ci/ref_spec.rb168
-rw-r--r--spec/models/ci/resource_group_spec.rb2
-rw-r--r--spec/models/ci/runner_manager_spec.rb64
-rw-r--r--spec/models/ci/runner_spec.rb37
-rw-r--r--spec/models/ci/stage_spec.rb12
-rw-r--r--spec/models/clusters/agent_spec.rb2
-rw-r--r--spec/models/clusters/platforms/kubernetes_spec.rb2
-rw-r--r--spec/models/commit_range_spec.rb2
-rw-r--r--spec/models/commit_spec.rb14
-rw-r--r--spec/models/commit_status_spec.rb22
-rw-r--r--spec/models/compare_spec.rb4
-rw-r--r--spec/models/concerns/awardable_spec.rb8
-rw-r--r--spec/models/concerns/case_sensitivity_spec.rb6
-rw-r--r--spec/models/concerns/ci/has_status_spec.rb24
-rw-r--r--spec/models/concerns/ci/partitionable/switch_spec.rb11
-rw-r--r--spec/models/concerns/enums/sbom_spec.rb3
-rw-r--r--spec/models/concerns/featurable_spec.rb2
-rw-r--r--spec/models/concerns/has_user_type_spec.rb2
-rw-r--r--spec/models/concerns/ignorable_columns_spec.rb4
-rw-r--r--spec/models/concerns/issuable_spec.rb4
-rw-r--r--spec/models/concerns/pg_full_text_searchable_spec.rb4
-rw-r--r--spec/models/concerns/project_features_compatibility_spec.rb6
-rw-r--r--spec/models/concerns/reactive_caching_spec.rb4
-rw-r--r--spec/models/concerns/reset_on_column_errors_spec.rb2
-rw-r--r--spec/models/concerns/sortable_spec.rb22
-rw-r--r--spec/models/concerns/token_authenticatable_spec.rb2
-rw-r--r--spec/models/concerns/use_sql_function_for_primary_key_lookups_spec.rb181
-rw-r--r--spec/models/container_repository_spec.rb105
-rw-r--r--spec/models/deployment_spec.rb10
-rw-r--r--spec/models/diff_viewer/base_spec.rb4
-rw-r--r--spec/models/email_spec.rb6
-rw-r--r--spec/models/environment_spec.rb82
-rw-r--r--spec/models/group_label_spec.rb2
-rw-r--r--spec/models/group_spec.rb40
-rw-r--r--spec/models/instance_configuration_spec.rb4
-rw-r--r--spec/models/integration_spec.rb17
-rw-r--r--spec/models/integrations/base_chat_notification_spec.rb6
-rw-r--r--spec/models/integrations/buildkite_spec.rb2
-rw-r--r--spec/models/integrations/campfire_spec.rb2
-rw-r--r--spec/models/integrations/integration_list_spec.rb4
-rw-r--r--spec/models/integrations/jira_spec.rb23
-rw-r--r--spec/models/integrations/teamcity_spec.rb4
-rw-r--r--spec/models/issue_spec.rb8
-rw-r--r--spec/models/member_spec.rb16
-rw-r--r--spec/models/members/members/members_with_parents_spec.rb92
-rw-r--r--spec/models/members/project_member_spec.rb44
-rw-r--r--spec/models/merge_request_diff_spec.rb2
-rw-r--r--spec/models/merge_request_spec.rb94
-rw-r--r--spec/models/ml/candidate_spec.rb40
-rw-r--r--spec/models/ml/model_metadata_spec.rb26
-rw-r--r--spec/models/ml/model_spec.rb43
-rw-r--r--spec/models/ml/model_version_spec.rb73
-rw-r--r--spec/models/namespace_setting_spec.rb68
-rw-r--r--spec/models/namespace_spec.rb55
-rw-r--r--spec/models/namespace_statistics_spec.rb2
-rw-r--r--spec/models/network/graph_spec.rb11
-rw-r--r--spec/models/notification_recipient_spec.rb4
-rw-r--r--spec/models/organizations/organization_spec.rb9
-rw-r--r--spec/models/packages/npm/metadata_cache_spec.rb36
-rw-r--r--spec/models/packages/nuget/symbol_spec.rb1
-rw-r--r--spec/models/packages/package_spec.rb2
-rw-r--r--spec/models/packages/protection/rule_spec.rb104
-rw-r--r--spec/models/packages/pypi/metadatum_spec.rb27
-rw-r--r--spec/models/packages/tag_spec.rb19
-rw-r--r--spec/models/pages/lookup_path_spec.rb20
-rw-r--r--spec/models/pages_deployment_spec.rb84
-rw-r--r--spec/models/pages_domain_spec.rb30
-rw-r--r--spec/models/personal_access_token_spec.rb2
-rw-r--r--spec/models/project_feature_spec.rb4
-rw-r--r--spec/models/project_feature_usage_spec.rb173
-rw-r--r--spec/models/project_label_spec.rb2
-rw-r--r--spec/models/project_setting_spec.rb2
-rw-r--r--spec/models/project_snippet_spec.rb15
-rw-r--r--spec/models/project_spec.rb227
-rw-r--r--spec/models/projects/repository_storage_move_spec.rb26
-rw-r--r--spec/models/projects/topic_spec.rb2
-rw-r--r--spec/models/prometheus_metric_spec.rb20
-rw-r--r--spec/models/releases/link_spec.rb2
-rw-r--r--spec/models/repository_spec.rb79
-rw-r--r--spec/models/service_desk/custom_email_credential_spec.rb33
-rw-r--r--spec/models/snippet_repository_spec.rb4
-rw-r--r--spec/models/snippet_spec.rb20
-rw-r--r--spec/models/terraform/state_spec.rb2
-rw-r--r--spec/models/upload_spec.rb4
-rw-r--r--spec/models/user_detail_spec.rb23
-rw-r--r--spec/models/user_spec.rb54
-rw-r--r--spec/models/users/anonymous_spec.rb (renamed from spec/models/guest_spec.rb)2
-rw-r--r--spec/models/users/credit_card_validation_spec.rb12
-rw-r--r--spec/models/users/group_visit_spec.rb25
-rw-r--r--spec/models/users/phone_number_validation_spec.rb46
-rw-r--r--spec/models/users/project_visit_spec.rb25
-rw-r--r--spec/models/vs_code/settings/vs_code_setting_spec.rb8
-rw-r--r--spec/models/web_ide_terminal_spec.rb6
-rw-r--r--spec/models/wiki_page_spec.rb25
-rw-r--r--spec/models/work_item_spec.rb6
-rw-r--r--spec/models/zoom_meeting_spec.rb4
-rw-r--r--spec/policies/abuse_report_policy_spec.rb2
-rw-r--r--spec/policies/ci/build_policy_spec.rb8
-rw-r--r--spec/policies/ci/pipeline_policy_spec.rb7
-rw-r--r--spec/policies/concerns/policy_actor_spec.rb6
-rw-r--r--spec/policies/global_policy_spec.rb7
-rw-r--r--spec/policies/group_group_link_policy_spec.rb63
-rw-r--r--spec/policies/issue_policy_spec.rb68
-rw-r--r--spec/policies/project_group_link_policy_spec.rb95
-rw-r--r--spec/policies/project_policy_spec.rb375
-rw-r--r--spec/policies/user_policy_spec.rb26
-rw-r--r--spec/policies/work_item_policy_spec.rb72
-rw-r--r--spec/presenters/ci/pipeline_artifacts/code_coverage_presenter_spec.rb8
-rw-r--r--spec/presenters/ci/pipeline_artifacts/code_quality_mr_diff_presenter_spec.rb8
-rw-r--r--spec/presenters/clusters/cluster_presenter_spec.rb2
-rw-r--r--spec/presenters/ml/model_presenter_spec.rb33
-rw-r--r--spec/presenters/ml/model_version_presenter_spec.rb28
-rw-r--r--spec/presenters/packages/nuget/packages_metadata_presenter_spec.rb2
-rw-r--r--spec/presenters/packages/nuget/search_results_presenter_spec.rb2
-rw-r--r--spec/presenters/project_presenter_spec.rb22
-rw-r--r--spec/presenters/projects/security/configuration_presenter_spec.rb2
-rw-r--r--spec/presenters/user_presenter_spec.rb22
-rw-r--r--spec/requests/acme_challenges_controller_spec.rb9
-rw-r--r--spec/requests/admin/users_controller_spec.rb50
-rw-r--r--spec/requests/api/badges_spec.rb2
-rw-r--r--spec/requests/api/bulk_imports_spec.rb15
-rw-r--r--spec/requests/api/ci/job_artifacts_spec.rb70
-rw-r--r--spec/requests/api/ci/jobs_spec.rb26
-rw-r--r--spec/requests/api/ci/pipelines_spec.rb154
-rw-r--r--spec/requests/api/ci/resource_groups_spec.rb4
-rw-r--r--spec/requests/api/ci/runner/jobs_artifacts_spec.rb11
-rw-r--r--spec/requests/api/ci/runner/jobs_request_post_spec.rb64
-rw-r--r--spec/requests/api/ci/runner/jobs_trace_spec.rb22
-rw-r--r--spec/requests/api/ci/runner/runners_post_spec.rb8
-rw-r--r--spec/requests/api/ci/runners_spec.rb51
-rw-r--r--spec/requests/api/ci/triggers_spec.rb2
-rw-r--r--spec/requests/api/commits_spec.rb10
-rw-r--r--spec/requests/api/container_repositories_spec.rb4
-rw-r--r--spec/requests/api/deployments_spec.rb2
-rw-r--r--spec/requests/api/geo_spec.rb2
-rw-r--r--spec/requests/api/graphql/abuse_report_spec.rb131
-rw-r--r--spec/requests/api/graphql/ci/catalog/resource_spec.rb341
-rw-r--r--spec/requests/api/graphql/ci/catalog/resources_spec.rb359
-rw-r--r--spec/requests/api/graphql/ci/manual_variables_spec.rb2
-rw-r--r--spec/requests/api/graphql/ci/runners_spec.rb87
-rw-r--r--spec/requests/api/graphql/container_repository/container_repository_details_spec.rb120
-rw-r--r--spec/requests/api/graphql/gitlab_schema_spec.rb6
-rw-r--r--spec/requests/api/graphql/group/container_repositories_spec.rb4
-rw-r--r--spec/requests/api/graphql/group/data_transfer_spec.rb42
-rw-r--r--spec/requests/api/graphql/group/milestones_spec.rb8
-rw-r--r--spec/requests/api/graphql/merge_requests/codequality_reports_comparer_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/admin/sidekiq_queues/delete_jobs_spec.rb12
-rw-r--r--spec/requests/api/graphql/mutations/alert_management/alerts/create_alert_issue_spec.rb32
-rw-r--r--spec/requests/api/graphql/mutations/award_emojis/toggle_spec.rb37
-rw-r--r--spec/requests/api/graphql/mutations/boards/issues/issue_move_list_spec.rb34
-rw-r--r--spec/requests/api/graphql/mutations/ci/catalog/resources/create_spec.rb40
-rw-r--r--spec/requests/api/graphql/mutations/ci/catalog/unpublish_spec.rb52
-rw-r--r--spec/requests/api/graphql/mutations/ci/pipeline_retry_spec.rb16
-rw-r--r--spec/requests/api/graphql/mutations/clusters/agent_tokens/agent_tokens/create_spec.rb4
-rw-r--r--spec/requests/api/graphql/mutations/clusters/agents/delete_spec.rb4
-rw-r--r--spec/requests/api/graphql/mutations/container_registry/protection/rule/create_spec.rb180
-rw-r--r--spec/requests/api/graphql/mutations/container_repository/destroy_tags_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/design_management/delete_spec.rb10
-rw-r--r--spec/requests/api/graphql/mutations/design_management/upload_spec.rb9
-rw-r--r--spec/requests/api/graphql/mutations/issues/link_alerts_spec.rb28
-rw-r--r--spec/requests/api/graphql/mutations/issues/move_spec.rb18
-rw-r--r--spec/requests/api/graphql/mutations/issues/set_confidential_spec.rb20
-rw-r--r--spec/requests/api/graphql/mutations/issues/set_crm_contacts_spec.rb26
-rw-r--r--spec/requests/api/graphql/mutations/issues/set_due_date_spec.rb22
-rw-r--r--spec/requests/api/graphql/mutations/issues/set_locked_spec.rb20
-rw-r--r--spec/requests/api/graphql/mutations/issues/set_severity_spec.rb20
-rw-r--r--spec/requests/api/graphql/mutations/issues/unlink_alerts_spec.rb28
-rw-r--r--spec/requests/api/graphql/mutations/merge_requests/reviewer_rereview_spec.rb12
-rw-r--r--spec/requests/api/graphql/mutations/merge_requests/set_assignees_spec.rb28
-rw-r--r--spec/requests/api/graphql/mutations/merge_requests/set_draft_spec.rb20
-rw-r--r--spec/requests/api/graphql/mutations/merge_requests/set_labels_spec.rb28
-rw-r--r--spec/requests/api/graphql/mutations/merge_requests/set_locked_spec.rb20
-rw-r--r--spec/requests/api/graphql/mutations/merge_requests/set_milestone_spec.rb24
-rw-r--r--spec/requests/api/graphql/mutations/merge_requests/set_reviewers_spec.rb28
-rw-r--r--spec/requests/api/graphql/mutations/merge_requests/set_time_estimate_spec.rb5
-rw-r--r--spec/requests/api/graphql/mutations/merge_requests/update_spec.rb17
-rw-r--r--spec/requests/api/graphql/mutations/namespace/package_settings/update_spec.rb20
-rw-r--r--spec/requests/api/graphql/mutations/notes/create/note_spec.rb43
-rw-r--r--spec/requests/api/graphql/mutations/notes/update/image_diff_note_spec.rb12
-rw-r--r--spec/requests/api/graphql/mutations/organizations/create_spec.rb64
-rw-r--r--spec/requests/api/graphql/mutations/packages/cleanup/policy/update_spec.rb18
-rw-r--r--spec/requests/api/graphql/mutations/packages/protection/rule/create_spec.rb232
-rw-r--r--spec/requests/api/graphql/mutations/packages/protection/rule/delete_spec.rb88
-rw-r--r--spec/requests/api/graphql/mutations/release_asset_links/update_spec.rb14
-rw-r--r--spec/requests/api/graphql/mutations/snippets/create_spec.rb10
-rw-r--r--spec/requests/api/graphql/mutations/snippets/destroy_spec.rb4
-rw-r--r--spec/requests/api/graphql/mutations/snippets/mark_as_spam_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/snippets/update_spec.rb38
-rw-r--r--spec/requests/api/graphql/mutations/todos/mark_all_done_spec.rb14
-rw-r--r--spec/requests/api/graphql/mutations/todos/mark_done_spec.rb20
-rw-r--r--spec/requests/api/graphql/mutations/todos/restore_many_spec.rb20
-rw-r--r--spec/requests/api/graphql/mutations/todos/restore_spec.rb20
-rw-r--r--spec/requests/api/graphql/namespace/root_storage_statistics_spec.rb9
-rw-r--r--spec/requests/api/graphql/organizations/organization_query_spec.rb7
-rw-r--r--spec/requests/api/graphql/project/base_service_spec.rb2
-rw-r--r--spec/requests/api/graphql/project/container_repositories_spec.rb14
-rw-r--r--spec/requests/api/graphql/project/data_transfer_spec.rb42
-rw-r--r--spec/requests/api/graphql/project/environments_spec.rb2
-rw-r--r--spec/requests/api/graphql/project/error_tracking/sentry_errors_request_spec.rb2
-rw-r--r--spec/requests/api/graphql/project/issue/design_collection/versions_spec.rb2
-rw-r--r--spec/requests/api/graphql/project/issue_spec.rb4
-rw-r--r--spec/requests/api/graphql/project/jira_import_spec.rb2
-rw-r--r--spec/requests/api/graphql/project/jira_projects_spec.rb8
-rw-r--r--spec/requests/api/graphql/project/merge_request_spec.rb2
-rw-r--r--spec/requests/api/graphql/project/merge_requests_spec.rb5
-rw-r--r--spec/requests/api/graphql/project/release_spec.rb24
-rw-r--r--spec/requests/api/graphql/project/terraform/state_spec.rb4
-rw-r--r--spec/requests/api/graphql/project/terraform/states_spec.rb4
-rw-r--r--spec/requests/api/graphql/project/work_items_spec.rb17
-rw-r--r--spec/requests/api/graphql/projects/projects_spec.rb77
-rw-r--r--spec/requests/api/graphql/user_spec.rb32
-rw-r--r--spec/requests/api/graphql/work_item_spec.rb75
-rw-r--r--spec/requests/api/group_packages_spec.rb23
-rw-r--r--spec/requests/api/groups_spec.rb38
-rw-r--r--spec/requests/api/helm_packages_spec.rb6
-rw-r--r--spec/requests/api/internal/base_spec.rb50
-rw-r--r--spec/requests/api/internal/pages_spec.rb42
-rw-r--r--spec/requests/api/issues/get_group_issues_spec.rb2
-rw-r--r--spec/requests/api/issues/get_project_issues_spec.rb2
-rw-r--r--spec/requests/api/issues/issues_spec.rb4
-rw-r--r--spec/requests/api/issues/post_projects_issues_spec.rb8
-rw-r--r--spec/requests/api/issues/put_projects_issues_spec.rb2
-rw-r--r--spec/requests/api/maven_packages_spec.rb32
-rw-r--r--spec/requests/api/members_spec.rb48
-rw-r--r--spec/requests/api/merge_request_approvals_spec.rb24
-rw-r--r--spec/requests/api/merge_requests_spec.rb8
-rw-r--r--spec/requests/api/metadata_spec.rb8
-rw-r--r--spec/requests/api/ml/mlflow/model_versions_spec.rb86
-rw-r--r--spec/requests/api/ml/mlflow/registered_models_spec.rb203
-rw-r--r--spec/requests/api/npm_project_packages_spec.rb4
-rw-r--r--spec/requests/api/pages/pages_spec.rb13
-rw-r--r--spec/requests/api/personal_access_tokens_spec.rb12
-rw-r--r--spec/requests/api/project_attributes.yml1
-rw-r--r--spec/requests/api/project_container_repositories_spec.rb10
-rw-r--r--spec/requests/api/project_packages_spec.rb20
-rw-r--r--spec/requests/api/project_repository_storage_moves_spec.rb24
-rw-r--r--spec/requests/api/project_templates_spec.rb4
-rw-r--r--spec/requests/api/projects_spec.rb67
-rw-r--r--spec/requests/api/pypi_packages_spec.rb17
-rw-r--r--spec/requests/api/releases_spec.rb2
-rw-r--r--spec/requests/api/repositories_spec.rb6
-rw-r--r--spec/requests/api/resource_access_tokens_spec.rb16
-rw-r--r--spec/requests/api/search_spec.rb4
-rw-r--r--spec/requests/api/settings_spec.rb10
-rw-r--r--spec/requests/api/tags_spec.rb2
-rw-r--r--spec/requests/api/task_completion_status_spec.rb16
-rw-r--r--spec/requests/api/unleash_spec.rb2
-rw-r--r--spec/requests/api/users_spec.rb44
-rw-r--r--spec/requests/api/v3/github_spec.rb721
-rw-r--r--spec/requests/api/vs_code/settings/vs_code_settings_sync_spec.rb89
-rw-r--r--spec/requests/api/wikis_spec.rb30
-rw-r--r--spec/requests/application_controller_spec.rb15
-rw-r--r--spec/requests/chaos_controller_spec.rb14
-rw-r--r--spec/requests/explore/catalog_controller_spec.rb53
-rw-r--r--spec/requests/external_redirect/external_redirect_controller_spec.rb60
-rw-r--r--spec/requests/health_controller_spec.rb4
-rw-r--r--spec/requests/jira_authorizations_spec.rb88
-rw-r--r--spec/requests/jwt_controller_spec.rb55
-rw-r--r--spec/requests/lfs_http_spec.rb6
-rw-r--r--spec/requests/lfs_locks_api_spec.rb8
-rw-r--r--spec/requests/metrics_controller_spec.rb9
-rw-r--r--spec/requests/oauth/authorizations_controller_spec.rb4
-rw-r--r--spec/requests/organizations/organizations_controller_spec.rb6
-rw-r--r--spec/requests/profiles/comment_templates_controller_spec.rb22
-rw-r--r--spec/requests/projects/merge_requests_controller_spec.rb15
-rw-r--r--spec/requests/projects/ml/model_versions_controller_spec.rb72
-rw-r--r--spec/requests/projects/ml/models_controller_spec.rb81
-rw-r--r--spec/requests/projects/service_desk_controller_spec.rb10
-rw-r--r--spec/requests/registrations_controller_spec.rb6
-rw-r--r--spec/requests/search_controller_spec.rb1
-rw-r--r--spec/requests/sessions_spec.rb4
-rw-r--r--spec/requests/users_controller_spec.rb4
-rw-r--r--spec/routing/organizations/organizations_controller_routing_spec.rb5
-rw-r--r--spec/routing/uploads_routing_spec.rb2
-rw-r--r--spec/rubocop/batched_background_migrations_dictionary_spec.rb (renamed from spec/rubocop/batched_background_migrations_spec.rb)32
-rw-r--r--spec/rubocop/cop/background_migration/dictionary_file_spec.rb (renamed from spec/rubocop/cop/background_migration/missing_dictionary_file_spec.rb)83
-rw-r--r--spec/rubocop/cop/gitlab/doc_url_spec.rb6
-rw-r--r--spec/rubocop/cop/gitlab/feature_available_usage_spec.rb2
-rw-r--r--spec/rubocop/cop/gitlab/mark_used_feature_flags_spec.rb34
-rw-r--r--spec/rubocop/cop/migration/migration_record_spec.rb2
-rw-r--r--spec/rubocop/cop/migration/migration_with_milestone_spec.rb99
-rw-r--r--spec/rubocop/cop/migration/prevent_index_creation_spec.rb2
-rw-r--r--spec/rubocop/cop/migration/sidekiq_queue_migrate_spec.rb2
-rw-r--r--spec/rubocop/cop/migration/unfinished_dependencies_spec.rb2
-rw-r--r--spec/rubocop/cop/migration/with_lock_retries_disallowed_method_spec.rb2
-rw-r--r--spec/rubocop/cop/performance/readlines_each_spec.rb2
-rw-r--r--spec/rubocop/cop/style/inline_disable_annotation_spec.rb46
-rw-r--r--spec/rubocop/rubocop_spec.rb13
-rw-r--r--spec/scripts/lib/glfm/update_specification_spec.rb2
-rw-r--r--spec/serializers/build_details_entity_spec.rb4
-rw-r--r--spec/serializers/ci/job_entity_spec.rb2
-rw-r--r--spec/serializers/ci/pipeline_entity_spec.rb1
-rw-r--r--spec/serializers/container_repositories_serializer_spec.rb2
-rw-r--r--spec/serializers/diff_file_entity_spec.rb2
-rw-r--r--spec/serializers/group_child_entity_spec.rb6
-rw-r--r--spec/serializers/group_link/group_group_link_entity_spec.rb65
-rw-r--r--spec/serializers/group_link/project_group_link_entity_spec.rb62
-rw-r--r--spec/serializers/issue_entity_spec.rb2
-rw-r--r--spec/serializers/merge_request_widget_entity_spec.rb2
-rw-r--r--spec/serializers/review_app_setup_entity_spec.rb4
-rw-r--r--spec/services/activity_pub/accept_follow_service_spec.rb77
-rw-r--r--spec/services/activity_pub/inbox_resolver_service_spec.rb99
-rw-r--r--spec/services/admin/plan_limits/update_service_spec.rb76
-rw-r--r--spec/services/application_settings/update_service_spec.rb6
-rw-r--r--spec/services/auto_merge/base_service_spec.rb24
-rw-r--r--spec/services/award_emojis/copy_service_spec.rb2
-rw-r--r--spec/services/bulk_imports/batched_relation_export_service_spec.rb23
-rw-r--r--spec/services/bulk_imports/file_download_service_spec.rb9
-rw-r--r--spec/services/bulk_imports/lfs_objects_export_service_spec.rb2
-rw-r--r--spec/services/bulk_imports/process_service_spec.rb25
-rw-r--r--spec/services/bulk_imports/relation_batch_export_service_spec.rb28
-rw-r--r--spec/services/bulk_imports/relation_export_service_spec.rb35
-rw-r--r--spec/services/ci/cancel_pipeline_service_spec.rb17
-rw-r--r--spec/services/ci/catalog/resources/create_service_spec.rb49
-rw-r--r--spec/services/ci/catalog/resources/release_service_spec.rb62
-rw-r--r--spec/services/ci/catalog/resources/validate_service_spec.rb77
-rw-r--r--spec/services/ci/catalog/resources/versions/create_service_spec.rb180
-rw-r--r--spec/services/ci/create_pipeline_service/pre_post_stages_spec.rb6
-rw-r--r--spec/services/ci/create_pipeline_service/rules_spec.rb2
-rw-r--r--spec/services/ci/create_pipeline_service_spec.rb2
-rw-r--r--spec/services/ci/enqueue_job_service_spec.rb29
-rw-r--r--spec/services/ci/pipeline_processing/atomic_processing_service_spec.rb88
-rw-r--r--spec/services/ci/pipelines/update_metadata_service_spec.rb34
-rw-r--r--spec/services/ci/play_build_service_spec.rb4
-rw-r--r--spec/services/ci/refs/enqueue_pipelines_to_unlock_service_spec.rb58
-rw-r--r--spec/services/ci/register_job_service_spec.rb8
-rw-r--r--spec/services/ci/retry_job_service_spec.rb16
-rw-r--r--spec/services/ci/retry_pipeline_service_spec.rb2
-rw-r--r--spec/services/ci/runners/register_runner_service_spec.rb4
-rw-r--r--spec/services/ci/stuck_builds/drop_pending_service_spec.rb2
-rw-r--r--spec/services/ci/stuck_builds/drop_running_service_spec.rb2
-rw-r--r--spec/services/ci/stuck_builds/drop_scheduled_service_spec.rb2
-rw-r--r--spec/services/clusters/kubernetes/create_or_update_service_account_service_spec.rb12
-rw-r--r--spec/services/container_registry/protection/create_rule_service_spec.rb145
-rw-r--r--spec/services/deployments/update_environment_service_spec.rb2
-rw-r--r--spec/services/design_management/copy_design_collection/copy_service_spec.rb2
-rw-r--r--spec/services/draft_notes/publish_service_spec.rb25
-rw-r--r--spec/services/environments/auto_recover_service_spec.rb99
-rw-r--r--spec/services/git/branch_hooks_service_spec.rb21
-rw-r--r--spec/services/git/branch_push_service_spec.rb2
-rw-r--r--spec/services/git/process_ref_changes_service_spec.rb2
-rw-r--r--spec/services/google_cloud/generate_pipeline_service_spec.rb16
-rw-r--r--spec/services/groups/update_statistics_service_spec.rb2
-rw-r--r--spec/services/import/gitlab_projects/create_project_service_spec.rb6
-rw-r--r--spec/services/import/validate_remote_git_endpoint_service_spec.rb43
-rw-r--r--spec/services/issuable/common_system_notes_service_spec.rb2
-rw-r--r--spec/services/issuable/discussions_list_service_spec.rb6
-rw-r--r--spec/services/issuable/process_assignees_spec.rb48
-rw-r--r--spec/services/issues/export_csv_service_spec.rb2
-rw-r--r--spec/services/issues/update_service_spec.rb19
-rw-r--r--spec/services/jira/requests/projects/list_service_spec.rb4
-rw-r--r--spec/services/jira_connect_subscriptions/create_service_spec.rb6
-rw-r--r--spec/services/lfs/file_transformer_spec.rb2
-rw-r--r--spec/services/merge_requests/conflicts/resolve_service_spec.rb8
-rw-r--r--spec/services/merge_requests/mark_reviewer_reviewed_service_spec.rb55
-rw-r--r--spec/services/merge_requests/merge_service_spec.rb4
-rw-r--r--spec/services/merge_requests/mergeability/check_ci_status_service_spec.rb41
-rw-r--r--spec/services/merge_requests/mergeability/check_discussions_status_service_spec.rb41
-rw-r--r--spec/services/merge_requests/mergeability/check_rebase_status_service_spec.rb41
-rw-r--r--spec/services/merge_requests/mergeability/run_checks_service_spec.rb20
-rw-r--r--spec/services/merge_requests/push_options_handler_service_spec.rb59
-rw-r--r--spec/services/merge_requests/pushed_branches_service_spec.rb4
-rw-r--r--spec/services/merge_requests/refresh_service_spec.rb2
-rw-r--r--spec/services/merge_requests/update_reviewer_state_service_spec.rb85
-rw-r--r--spec/services/merge_requests/update_service_spec.rb4
-rw-r--r--spec/services/ml/create_candidate_service_spec.rb57
-rw-r--r--spec/services/ml/create_model_service_spec.rb81
-rw-r--r--spec/services/ml/find_model_service_spec.rb29
-rw-r--r--spec/services/ml/find_or_create_model_service_spec.rb5
-rw-r--r--spec/services/ml/find_or_create_model_version_service_spec.rb12
-rw-r--r--spec/services/ml/model_versions/get_model_version_service_spec.rb28
-rw-r--r--spec/services/ml/update_model_service_spec.rb27
-rw-r--r--spec/services/notes/create_service_spec.rb73
-rw-r--r--spec/services/organizations/create_service_spec.rb40
-rw-r--r--spec/services/packages/create_dependency_service_spec.rb16
-rw-r--r--spec/services/packages/npm/create_package_service_spec.rb364
-rw-r--r--spec/services/packages/npm/generate_metadata_service_spec.rb2
-rw-r--r--spec/services/packages/nuget/check_duplicates_service_spec.rb4
-rw-r--r--spec/services/packages/nuget/create_dependency_service_spec.rb6
-rw-r--r--spec/services/packages/nuget/extract_metadata_file_service_spec.rb14
-rw-r--r--spec/services/packages/nuget/metadata_extraction_service_spec.rb7
-rw-r--r--spec/services/packages/nuget/process_package_file_service_spec.rb41
-rw-r--r--spec/services/packages/nuget/symbols/create_symbol_files_service_spec.rb46
-rw-r--r--spec/services/packages/nuget/symbols/extract_signature_and_checksum_service_spec.rb46
-rw-r--r--spec/services/packages/nuget/symbols/extract_symbol_signature_service_spec.rb23
-rw-r--r--spec/services/packages/nuget/update_package_from_metadata_service_spec.rb37
-rw-r--r--spec/services/packages/protection/create_rule_service_spec.rb2
-rw-r--r--spec/services/packages/protection/delete_rule_service_spec.rb92
-rw-r--r--spec/services/packages/pypi/create_package_service_spec.rb24
-rw-r--r--spec/services/packages/update_tags_service_spec.rb2
-rw-r--r--spec/services/pages/delete_service_spec.rb22
-rw-r--r--spec/services/pages_domains/obtain_lets_encrypt_certificate_service_spec.rb2
-rw-r--r--spec/services/product_analytics/build_graph_service_spec.rb2
-rw-r--r--spec/services/projects/branches_by_mode_service_spec.rb2
-rw-r--r--spec/services/projects/container_repository/delete_tags_service_spec.rb4
-rw-r--r--spec/services/projects/container_repository/gitlab/delete_tags_service_spec.rb6
-rw-r--r--spec/services/projects/container_repository/third_party/delete_tags_service_spec.rb4
-rw-r--r--spec/services/projects/create_service_spec.rb2
-rw-r--r--spec/services/projects/destroy_service_spec.rb25
-rw-r--r--spec/services/projects/fork_service_spec.rb20
-rw-r--r--spec/services/projects/group_links/create_service_spec.rb95
-rw-r--r--spec/services/projects/group_links/destroy_service_spec.rb143
-rw-r--r--spec/services/projects/group_links/update_service_spec.rb121
-rw-r--r--spec/services/projects/lfs_pointers/lfs_link_service_spec.rb6
-rw-r--r--spec/services/projects/operations/update_service_spec.rb2
-rw-r--r--spec/services/projects/record_target_platforms_service_spec.rb12
-rw-r--r--spec/services/projects/update_pages_service_spec.rb82
-rw-r--r--spec/services/projects/update_repository_storage_service_spec.rb24
-rw-r--r--spec/services/projects/update_statistics_service_spec.rb14
-rw-r--r--spec/services/quick_actions/interpret_service_spec.rb82
-rw-r--r--spec/services/releases/create_service_spec.rb20
-rw-r--r--spec/services/service_desk/custom_email_verifications/update_service_spec.rb27
-rw-r--r--spec/services/service_desk/custom_emails/create_service_spec.rb29
-rw-r--r--spec/services/service_desk_settings/update_service_spec.rb28
-rw-r--r--spec/services/spam/spam_action_service_spec.rb59
-rw-r--r--spec/services/system_notes/issuables_service_spec.rb4
-rw-r--r--spec/services/upload_service_spec.rb2
-rw-r--r--spec/services/users/refresh_authorized_projects_service_spec.rb7
-rw-r--r--spec/services/users/upsert_credit_card_validation_service_spec.rb111
-rw-r--r--spec/services/vs_code/settings/delete_service_spec.rb21
-rw-r--r--spec/services/web_hook_service_spec.rb62
-rw-r--r--spec/sidekiq_cluster/sidekiq_cluster_spec.rb18
-rw-r--r--spec/spec_helper.rb15
-rw-r--r--spec/support/atlassian/jira_connect/schemata.rb36
-rw-r--r--spec/support/capybara.rb2
-rw-r--r--spec/support/capybara_slow_finder.rb4
-rw-r--r--spec/support/database/auto_explain.rb5
-rw-r--r--spec/support/database/click_house/hooks.rb34
-rw-r--r--spec/support/database/partitioning_routing_analyzer.rb7
-rw-r--r--spec/support/db_cleaner.rb6
-rw-r--r--spec/support/finder_collection_allowlist.yml1
-rw-r--r--spec/support/helpers/api_internal_base_helpers.rb10
-rw-r--r--spec/support/helpers/click_house_test_helpers.rb85
-rw-r--r--spec/support/helpers/crypto_helpers.rb7
-rw-r--r--spec/support/helpers/cycle_analytics_helpers.rb25
-rw-r--r--spec/support/helpers/cycle_analytics_helpers/test_generation.rb166
-rw-r--r--spec/support/helpers/database/duplicate_indexes.yml197
-rw-r--r--spec/support/helpers/email_helpers.rb3
-rw-r--r--spec/support/helpers/features/dom_helpers.rb4
-rw-r--r--spec/support/helpers/features/releases_helpers.rb16
-rw-r--r--spec/support/helpers/gitaly_setup.rb4
-rw-r--r--spec/support/helpers/gpg_helpers.rb2
-rw-r--r--spec/support/helpers/graphql_helpers.rb1
-rw-r--r--spec/support/helpers/listbox_helpers.rb4
-rw-r--r--spec/support/helpers/login_helpers.rb29
-rw-r--r--spec/support/helpers/navbar_structure_helper.rb82
-rw-r--r--spec/support/helpers/packages_manager_api_spec_helpers.rb (renamed from spec/support/helpers/packages_manager_api_spec_helper.rb)4
-rw-r--r--spec/support/helpers/prevent_set_operator_mismatch_helper.rb16
-rw-r--r--spec/support/helpers/project_template_test_helper.rb1
-rw-r--r--spec/support/helpers/prometheus_helpers.rb2
-rw-r--r--spec/support/helpers/repo_helpers.rb4
-rw-r--r--spec/support/helpers/search_helpers.rb26
-rw-r--r--spec/support/helpers/seed_repo.rb2
-rw-r--r--spec/support/helpers/stub_saas_features.rb4
-rw-r--r--spec/support/helpers/test_env.rb8
-rw-r--r--spec/support/helpers/usage_data_helpers.rb8
-rw-r--r--spec/support/import_export/configuration_helper.rb2
-rw-r--r--spec/support/import_export/export_file_helper.rb2
-rw-r--r--spec/support/matchers/markdown_matchers.rb2
-rw-r--r--spec/support/matchers/navigation_matcher.rb12
-rw-r--r--spec/support/redis.rb7
-rw-r--r--spec/support/rspec_order_todo.yml390
-rw-r--r--spec/support/shared_contexts/ci/catalog/resources/version_shared_context.rb33
-rw-r--r--spec/support/shared_contexts/controllers/ambiguous_ref_controller_shared_context.rb19
-rw-r--r--spec/support/shared_contexts/features/integrations/project_integrations_shared_context.rb2
-rw-r--r--spec/support/shared_contexts/graphql/resolvers/runners_resolver_shared_context.rb6
-rw-r--r--spec/support/shared_contexts/graphql/types/query_type_shared_context.rb2
-rw-r--r--spec/support/shared_contexts/lib/gitlab/sidekiq_middleware/server_metrics_shared_context.rb25
-rw-r--r--spec/support/shared_contexts/models/ci/job_token_scope.rb2
-rw-r--r--spec/support/shared_contexts/navbar_structure_context.rb172
-rw-r--r--spec/support/shared_contexts/policies/project_policy_shared_context.rb2
-rw-r--r--spec/support/shared_contexts/services/projects/container_repository/delete_tags_service_shared_context.rb22
-rw-r--r--spec/support/shared_examples/analytics/cycle_analytics/request_params_examples.rb12
-rw-r--r--spec/support/shared_examples/boards/multiple_issue_boards_shared_examples.rb6
-rw-r--r--spec/support/shared_examples/ci/deployable_policy_shared_examples.rb1
-rw-r--r--spec/support/shared_examples/ci/deployable_policy_shared_examples_ee.rb6
-rw-r--r--spec/support/shared_examples/ci/redis_shared_examples.rb222
-rw-r--r--spec/support/shared_examples/controllers/base_action_controller_shared_examples.rb26
-rw-r--r--spec/support/shared_examples/controllers/is_ambiguous_ref_examples.rb55
-rw-r--r--spec/support/shared_examples/controllers/wiki_actions_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/database_health_status_indicators/prometheus_alert_based_shared_examples.rb4
-rw-r--r--spec/support/shared_examples/features/2fa_shared_examples.rb6
-rw-r--r--spec/support/shared_examples/features/content_editor_shared_examples.rb89
-rw-r--r--spec/support/shared_examples/features/dashboard/sidebar_shared_examples.rb12
-rw-r--r--spec/support/shared_examples/features/explore/sidebar_shared_examples.rb28
-rw-r--r--spec/support/shared_examples/features/issuable_invite_members_shared_examples.rb26
-rw-r--r--spec/support/shared_examples/features/nav_sidebar_shared_examples.rb11
-rw-r--r--spec/support/shared_examples/features/navbar_shared_examples.rb13
-rw-r--r--spec/support/shared_examples/features/packages_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/features/page_description_shared_examples.rb4
-rw-r--r--spec/support/shared_examples/features/project_features_apply_to_issuables_shared_examples.rb4
-rw-r--r--spec/support/shared_examples/features/search/search_timeouts_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/features/snippets_shared_examples.rb26
-rw-r--r--spec/support/shared_examples/features/variable_list_env_scope_shared_examples.rb87
-rw-r--r--spec/support/shared_examples/features/variable_list_shared_examples.rb292
-rw-r--r--spec/support/shared_examples/features/wiki/user_creates_wiki_page_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/features/wiki/user_updates_wiki_page_shared_examples.rb9
-rw-r--r--spec/support/shared_examples/features/wiki/user_views_wiki_page_shared_examples.rb17
-rw-r--r--spec/support/shared_examples/features/work_items_shared_examples.rb225
-rw-r--r--spec/support/shared_examples/finders/issues_finder_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/graphql/notes_quick_actions_for_work_items_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/graphql/resolvers/data_transfer_resolver_shared_examples.rb10
-rw-r--r--spec/support/shared_examples/graphql/resolvers/packages_resolvers_shared_examples.rb12
-rw-r--r--spec/support/shared_examples/graphql/resolvers/users/pages_visits_resolvers_shared_examples.rb39
-rw-r--r--spec/support/shared_examples/graphql/types/merge_request_interactions_type_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/lib/banzai/filters/sanitization_filter_shared_examples.rb22
-rw-r--r--spec/support/shared_examples/lib/gitlab/import/advance_stage_shared_examples.rb10
-rw-r--r--spec/support/shared_examples/lib/gitlab/middleware/multipart_shared_examples.rb24
-rw-r--r--spec/support/shared_examples/lib/sbom/package_url_shared_examples.rb23
-rw-r--r--spec/support/shared_examples/lib/wikis_api_examples.rb6
-rw-r--r--spec/support/shared_examples/mailers/notify_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/metrics/transaction_metrics_with_labels_shared_examples.rb6
-rw-r--r--spec/support/shared_examples/models/application_setting_shared_examples.rb18
-rw-r--r--spec/support/shared_examples/models/concerns/can_move_repository_storage_shared_examples.rb6
-rw-r--r--spec/support/shared_examples/models/concerns/repository_storage_movable_shared_examples.rb52
-rw-r--r--spec/support/shared_examples/models/diff_positionable_note_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/models/project_ci_cd_settings_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/models/users/pages_visits_shared_examples.rb104
-rw-r--r--spec/support/shared_examples/models/wiki_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/path_extraction_shared_examples.rb4
-rw-r--r--spec/support/shared_examples/quick_actions/commit/tag_quick_action_shared_examples.rb4
-rw-r--r--spec/support/shared_examples/quick_actions/issuable/close_quick_action_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/quick_actions/issue/create_merge_request_quick_action_shared_examples.rb4
-rw-r--r--spec/support/shared_examples/redis/redis_shared_examples.rb15
-rw-r--r--spec/support/shared_examples/requests/api/container_repositories_shared_examples.rb8
-rw-r--r--spec/support/shared_examples/requests/api/graphql/issue_list_shared_examples.rb14
-rw-r--r--spec/support/shared_examples/requests/api/graphql/remote_development_shared_examples.rb48
-rw-r--r--spec/support/shared_examples/requests/api/graphql/work_item_list_shared_examples.rb8
-rw-r--r--spec/support/shared_examples/requests/api/helm_packages_shared_examples.rb10
-rw-r--r--spec/support/shared_examples/requests/api/integrations/github_enterprise_jira_dvcs_end_of_life_shared_examples.rb23
-rw-r--r--spec/support/shared_examples/requests/api/ml/mlflow/mlflow_shared_examples.rb31
-rw-r--r--spec/support/shared_examples/requests/api/multiple_and_scoped_issue_boards_shared_examples.rb1
-rw-r--r--spec/support/shared_examples/requests/api/nuget_packages_shared_examples.rb9
-rw-r--r--spec/support/shared_examples/requests/api/repository_storage_moves_shared_examples.rb100
-rw-r--r--spec/support/shared_examples/requests/api/rubygems_packages_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/sends_git_audit_streaming_event_shared_examples.rb41
-rw-r--r--spec/support/shared_examples/serializers/environment_serializer_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/services/container_registry_auth_service_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/services/notification_service_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/services/protected_branches_shared_examples.rb31
-rw-r--r--spec/support/shared_examples/validators/url_validator_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/views/themed_layout_examples.rb6
-rw-r--r--spec/support/sidekiq_middleware.rb2
-rw-r--r--spec/support_specs/graphql/arguments_spec.rb2
-rw-r--r--spec/support_specs/helpers/active_record/query_recorder_spec.rb2
-rw-r--r--spec/support_specs/helpers/migrations_helpers_spec.rb4
-rw-r--r--spec/support_specs/helpers/stub_saas_features_spec.rb6
-rw-r--r--spec/support_specs/matchers/exceed_query_limit_helpers_spec.rb24
-rw-r--r--spec/tasks/gitlab/background_migrations_rake_spec.rb1
-rw-r--r--spec/tasks/gitlab/backup_rake_spec.rb4
-rw-r--r--spec/tasks/gitlab/click_house/migration_rake_spec.rb133
-rw-r--r--spec/tasks/gitlab/db_rake_spec.rb24
-rw-r--r--spec/tasks/gitlab/feature_categories_rake_spec.rb10
-rw-r--r--spec/tasks/gitlab/redis_rake_spec.rb188
-rw-r--r--spec/tooling/danger/analytics_instrumentation_spec.rb62
-rw-r--r--spec/tooling/danger/bulk_database_actions_spec.rb6
-rw-r--r--spec/tooling/danger/change_column_default_spec.rb104
-rw-r--r--spec/tooling/danger/clickhouse_spec.rb4
-rw-r--r--spec/tooling/danger/config_files_spec.rb4
-rw-r--r--spec/tooling/danger/customer_success_spec.rb28
-rw-r--r--spec/tooling/danger/database_dictionary_spec.rb2
-rw-r--r--spec/tooling/danger/database_spec.rb4
-rw-r--r--spec/tooling/danger/datateam_spec.rb60
-rw-r--r--spec/tooling/danger/experiments_spec.rb2
-rw-r--r--spec/tooling/danger/feature_flag_spec.rb4
-rw-r--r--spec/tooling/danger/gitlab_schema_validation_suggestion_spec.rb108
-rw-r--r--spec/tooling/danger/ignored_model_columns_spec.rb2
-rw-r--r--spec/tooling/danger/model_validations_spec.rb4
-rw-r--r--spec/tooling/danger/multiversion_spec.rb2
-rw-r--r--spec/tooling/danger/outdated_todo_spec.rb83
-rw-r--r--spec/tooling/danger/project_helper_spec.rb4
-rw-r--r--spec/tooling/danger/required_stops_spec.rb4
-rw-r--r--spec/tooling/danger/rubocop_inline_disable_suggestion_spec.rb39
-rw-r--r--spec/tooling/danger/saas_feature_spec.rb2
-rw-r--r--spec/tooling/danger/sidekiq_args_spec.rb4
-rw-r--r--spec/tooling/danger/sidekiq_queues_spec.rb14
-rw-r--r--spec/tooling/danger/specs/feature_category_suggestion_spec.rb1
-rw-r--r--spec/tooling/danger/specs/match_with_array_suggestion_spec.rb1
-rw-r--r--spec/tooling/danger/specs/project_factory_suggestion_spec.rb1
-rw-r--r--spec/tooling/danger/specs_spec.rb1
-rw-r--r--spec/tooling/danger/stable_branch_spec.rb4
-rw-r--r--spec/tooling/fixtures/change_column_default_migration.txt13
-rw-r--r--spec/tooling/lib/tooling/find_changes_spec.rb35
-rw-r--r--spec/tooling/lib/tooling/test_map_generator_spec.rb2
-rw-r--r--spec/tooling/quality/test_level_spec.rb4
-rw-r--r--spec/uploaders/attachment_uploader_spec.rb10
-rw-r--r--spec/uploaders/avatar_uploader_spec.rb10
-rw-r--r--spec/uploaders/ci/pipeline_artifact_uploader_spec.rb4
-rw-r--r--spec/uploaders/dependency_proxy/file_uploader_spec.rb4
-rw-r--r--spec/uploaders/design_management/design_v432x230_uploader_spec.rb14
-rw-r--r--spec/uploaders/external_diff_uploader_spec.rb8
-rw-r--r--spec/uploaders/import_export_uploader_spec.rb4
-rw-r--r--spec/uploaders/job_artifact_uploader_spec.rb4
-rw-r--r--spec/uploaders/lfs_object_uploader_spec.rb4
-rw-r--r--spec/uploaders/namespace_file_uploader_spec.rb6
-rw-r--r--spec/uploaders/object_storage_spec.rb4
-rw-r--r--spec/uploaders/packages/composer/cache_uploader_spec.rb4
-rw-r--r--spec/uploaders/packages/debian/component_file_uploader_spec.rb8
-rw-r--r--spec/uploaders/packages/debian/distribution_release_file_uploader_spec.rb8
-rw-r--r--spec/uploaders/packages/package_file_uploader_spec.rb4
-rw-r--r--spec/uploaders/pages/deployment_uploader_spec.rb4
-rw-r--r--spec/uploaders/personal_file_uploader_spec.rb14
-rw-r--r--spec/validators/any_field_validator_spec.rb4
-rw-r--r--spec/validators/ip_cidr_array_validator_spec.rb45
-rw-r--r--spec/validators/ip_cidr_validator_spec.rb45
-rw-r--r--spec/views/admin/application_settings/_repository_storage.html.haml_spec.rb2
-rw-r--r--spec/views/ci/status/_badge.html.haml_spec.rb92
-rw-r--r--spec/views/ci/status/_icon.html.haml_spec.rb6
-rw-r--r--spec/views/groups/edit.html.haml_spec.rb1
-rw-r--r--spec/views/layouts/_head.html.haml_spec.rb10
-rw-r--r--spec/views/layouts/application.html.haml_spec.rb1
-rw-r--r--spec/views/layouts/header/_new_dropdown.haml_spec.rb5
-rw-r--r--spec/views/layouts/header/_super_sidebar_logged_out.html.haml_spec.rb1
-rw-r--r--spec/views/layouts/nav/sidebar/_project.html.haml_spec.rb4
-rw-r--r--spec/views/layouts/snippets.html.haml_spec.rb32
-rw-r--r--spec/views/projects/commit/branches.html.haml_spec.rb2
-rw-r--r--spec/views/projects/commits/_commit.html.haml_spec.rb6
-rw-r--r--spec/views/projects/issues/_related_branches.html.haml_spec.rb3
-rw-r--r--spec/views/projects/pages/new.html.haml_spec.rb28
-rw-r--r--spec/views/projects/tags/index.html.haml_spec.rb2
-rw-r--r--spec/views/search/show.html.haml_spec.rb7
-rw-r--r--spec/workers/abuse/spam_abuse_events_worker_spec.rb85
-rw-r--r--spec/workers/activity_pub/projects/releases_subscription_worker_spec.rb128
-rw-r--r--spec/workers/bulk_import_worker_spec.rb14
-rw-r--r--spec/workers/bulk_imports/entity_worker_spec.rb54
-rw-r--r--spec/workers/bulk_imports/export_request_worker_spec.rb3
-rw-r--r--spec/workers/bulk_imports/finish_batched_pipeline_worker_spec.rb117
-rw-r--r--spec/workers/bulk_imports/pipeline_batch_worker_spec.rb213
-rw-r--r--spec/workers/bulk_imports/pipeline_worker_spec.rb341
-rw-r--r--spec/workers/bulk_imports/relation_batch_export_worker_spec.rb19
-rw-r--r--spec/workers/bulk_imports/relation_export_worker_spec.rb16
-rw-r--r--spec/workers/bulk_imports/stuck_import_worker_spec.rb31
-rw-r--r--spec/workers/ci/cancel_pipeline_worker_spec.rb24
-rw-r--r--spec/workers/ci/initial_pipeline_process_worker_spec.rb26
-rw-r--r--spec/workers/ci/pipeline_success_unlock_artifacts_worker_spec.rb3
-rw-r--r--spec/workers/ci/refs/unlock_previous_pipelines_worker_spec.rb21
-rw-r--r--spec/workers/concerns/gitlab/github_import/stage_methods_spec.rb30
-rw-r--r--spec/workers/concerns/worker_attributes_spec.rb2
-rw-r--r--spec/workers/concerns/worker_context_spec.rb4
-rw-r--r--spec/workers/container_registry/migration/enqueuer_worker_spec.rb6
-rw-r--r--spec/workers/environments/auto_recover_worker_spec.rb68
-rw-r--r--spec/workers/environments/auto_stop_cron_worker_spec.rb8
-rw-r--r--spec/workers/every_sidekiq_worker_spec.rb19
-rw-r--r--spec/workers/gitlab/bitbucket_import/advance_stage_worker_spec.rb2
-rw-r--r--spec/workers/gitlab/github_import/stage/import_attachments_worker_spec.rb4
-rw-r--r--spec/workers/gitlab/github_import/stage/import_base_data_worker_spec.rb2
-rw-r--r--spec/workers/gitlab/github_import/stage/import_collaborators_worker_spec.rb8
-rw-r--r--spec/workers/gitlab/github_import/stage/import_issue_events_worker_spec.rb4
-rw-r--r--spec/workers/gitlab/github_import/stage/import_issues_and_diff_notes_worker_spec.rb2
-rw-r--r--spec/workers/gitlab/github_import/stage/import_lfs_objects_worker_spec.rb2
-rw-r--r--spec/workers/gitlab/github_import/stage/import_notes_worker_spec.rb2
-rw-r--r--spec/workers/gitlab/github_import/stage/import_protected_branches_worker_spec.rb5
-rw-r--r--spec/workers/gitlab/github_import/stage/import_pull_requests_merged_by_worker_spec.rb5
-rw-r--r--spec/workers/gitlab/github_import/stage/import_pull_requests_review_requests_worker_spec.rb5
-rw-r--r--spec/workers/gitlab/github_import/stage/import_pull_requests_reviews_worker_spec.rb4
-rw-r--r--spec/workers/gitlab/github_import/stage/import_pull_requests_worker_spec.rb13
-rw-r--r--spec/workers/gitlab/jira_import/stage/import_issues_worker_spec.rb40
-rw-r--r--spec/workers/groups/update_statistics_worker_spec.rb2
-rw-r--r--spec/workers/jira_connect/sync_branch_worker_spec.rb2
-rw-r--r--spec/workers/merge_request_cleanup_refs_worker_spec.rb12
-rw-r--r--spec/workers/merge_requests/set_reviewer_reviewed_worker_spec.rb10
-rw-r--r--spec/workers/packages/cleanup_package_registry_worker_spec.rb22
-rw-r--r--spec/workers/packages/npm/cleanup_stale_metadata_cache_worker_spec.rb79
-rw-r--r--spec/workers/post_receive_spec.rb2
-rw-r--r--spec/workers/project_cache_worker_spec.rb14
-rw-r--r--spec/workers/projects/import_export/after_import_merge_requests_worker_spec.rb23
-rw-r--r--spec/workers/projects/record_target_platforms_worker_spec.rb2
-rw-r--r--spec/workers/repository_fork_worker_spec.rb30
-rw-r--r--spec/workers/schedule_merge_request_cleanup_refs_worker_spec.rb12
-rw-r--r--spec/workers/stuck_merge_jobs_worker_spec.rb6
-rw-r--r--spec/workers/tasks_to_be_done/create_worker_spec.rb22
-rw-r--r--spec/workers/update_project_statistics_worker_spec.rb2
1819 files changed, 35641 insertions, 18646 deletions
diff --git a/spec/click_house/migration_support/migration_context_spec.rb b/spec/click_house/migration_support/migration_context_spec.rb
new file mode 100644
index 00000000000..48ad9d9e3fa
--- /dev/null
+++ b/spec/click_house/migration_support/migration_context_spec.rb
@@ -0,0 +1,233 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+require_relative '../../../lib/click_house/migration_support/migration_error'
+
+RSpec.describe ClickHouse::MigrationSupport::MigrationContext,
+ click_house: :without_migrations, feature_category: :database do
+ include ClickHouseTestHelpers
+
+ # We don't need to delete data since we don't modify Postgres data
+ self.use_transactional_tests = false
+
+ let_it_be(:schema_migration) { ClickHouse::MigrationSupport::SchemaMigration }
+
+ let(:migrations_base_dir) { 'click_house/migrations' }
+ let(:migrations_dir) { expand_fixture_path("#{migrations_base_dir}/#{migrations_dirname}") }
+ let(:migration_context) { described_class.new(migrations_dir, schema_migration) }
+ let(:target_version) { nil }
+
+ after do
+ clear_consts(expand_fixture_path(migrations_base_dir))
+ end
+
+ describe 'performs migrations' do
+ subject(:migration) { migrate(target_version, migration_context) }
+
+ describe 'when creating a table' do
+ let(:migrations_dirname) { 'plain_table_creation' }
+
+ it 'creates a table' do
+ expect { migration }.to change { active_schema_migrations_count }.from(0).to(1)
+
+ table_schema = describe_table('some')
+ expect(schema_migrations).to contain_exactly(a_hash_including(version: '1', active: 1))
+ expect(table_schema).to match({
+ id: a_hash_including(type: 'UInt64'),
+ date: a_hash_including(type: 'Date')
+ })
+ end
+ end
+
+ describe 'when dropping a table' do
+ let(:migrations_dirname) { 'drop_table' }
+ let(:target_version) { 2 }
+
+ it 'drops table' do
+ migrate(1, migration_context)
+ expect(table_names).to include('some')
+
+ migration
+ expect(table_names).not_to include('some')
+ end
+ end
+
+ context 'when a migration raises an error' do
+ let(:migrations_dirname) { 'migration_with_error' }
+
+ it 'passes the error to caller as a StandardError' do
+ expect { migration }.to raise_error StandardError,
+ "An error has occurred, all later migrations canceled:\n\nA migration error happened"
+ expect(schema_migrations).to be_empty
+ end
+ end
+
+ context 'when a migration targets an unknown database' do
+ let(:migrations_dirname) { 'plain_table_creation_on_invalid_database' }
+
+ it 'raises ConfigurationError' do
+ expect { migration }.to raise_error ClickHouse::Client::ConfigurationError,
+ "The database 'unknown_database' is not configured"
+ end
+ end
+
+ context 'when migrations target multiple databases' do
+ let_it_be(:config) { ClickHouse::Client::Configuration.new }
+ let_it_be(:main_db_config) { [:main, config] }
+ let_it_be(:another_db_config) { [:another_db, config] }
+ let_it_be(:another_database_name) { 'gitlab_clickhouse_test_2' }
+
+ let(:migrations_dirname) { 'migrations_over_multiple_databases' }
+
+ before(:context) do
+ # Ensure we have a second database to run the test on
+ clone_database_configuration(:main, :another_db, another_database_name, config)
+
+ with_net_connect_allowed do
+ ClickHouse::Client.execute("CREATE DATABASE IF NOT EXISTS #{another_database_name}", :main, config)
+ end
+ end
+
+ after(:context) do
+ with_net_connect_allowed do
+ ClickHouse::Client.execute("DROP DATABASE #{another_database_name}", :another_db, config)
+ end
+ end
+
+ around do |example|
+ clear_db(config)
+
+ previous_config = ClickHouse::Migration.client_configuration
+ ClickHouse::Migration.client_configuration = config
+
+ example.run
+ ensure
+ ClickHouse::Migration.client_configuration = previous_config
+ end
+
+ def clone_database_configuration(source_db_identifier, target_db_identifier, target_db_name, target_config)
+ raw_config = Rails.application.config_for(:click_house)
+ raw_config.each do |database_identifier, db_config|
+ register_database(target_config, database_identifier, db_config)
+ end
+
+ target_db_config = raw_config[source_db_identifier].merge(database: target_db_name)
+ register_database(target_config, target_db_identifier, target_db_config)
+ target_config.http_post_proc = ClickHouse::Client.configuration.http_post_proc
+ target_config.json_parser = ClickHouse::Client.configuration.json_parser
+ target_config.logger = ::Logger.new(IO::NULL)
+ end
+
+ it 'registers migrations on respective database', :aggregate_failures do
+ expect { migrate(2, migration_context) }
+ .to change { active_schema_migrations_count(*main_db_config) }.from(0).to(1)
+ .and change { active_schema_migrations_count(*another_db_config) }.from(0).to(1)
+
+ expect(schema_migrations(*another_db_config)).to contain_exactly(a_hash_including(version: '2', active: 1))
+ expect(table_names(*main_db_config)).not_to include('some_on_another_db')
+ expect(table_names(*another_db_config)).not_to include('some')
+
+ expect(describe_table('some', *main_db_config)).to match({
+ id: a_hash_including(type: 'UInt64'),
+ date: a_hash_including(type: 'Date')
+ })
+ expect(describe_table('some_on_another_db', *another_db_config)).to match({
+ id: a_hash_including(type: 'UInt64'),
+ date: a_hash_including(type: 'Date')
+ })
+
+ expect { migrate(nil, migration_context) }
+ .to change { active_schema_migrations_count(*main_db_config) }.to(2)
+ .and not_change { active_schema_migrations_count(*another_db_config) }
+
+ expect(schema_migrations(*main_db_config)).to match([
+ a_hash_including(version: '1', active: 1),
+ a_hash_including(version: '3', active: 1)
+ ])
+ expect(schema_migrations(*another_db_config)).to match_array(a_hash_including(version: '2', active: 1))
+
+ expect(describe_table('some', *main_db_config)).to match({
+ id: a_hash_including(type: 'UInt64'),
+ timestamp: a_hash_including(type: 'Date')
+ })
+ end
+ end
+
+ context 'when target_version is incorrect' do
+ let(:target_version) { 2 }
+ let(:migrations_dirname) { 'plain_table_creation' }
+
+ it 'raises UnknownMigrationVersionError' do
+ expect { migration }.to raise_error ClickHouse::MigrationSupport::UnknownMigrationVersionError
+
+ expect(active_schema_migrations_count).to eq 0
+ end
+ end
+
+ context 'when migrations with duplicate name exist' do
+ let(:migrations_dirname) { 'duplicate_name' }
+
+ it 'raises DuplicateMigrationNameError' do
+ expect { migration }.to raise_error ClickHouse::MigrationSupport::DuplicateMigrationNameError
+
+ expect(active_schema_migrations_count).to eq 0
+ end
+ end
+
+ context 'when migrations with duplicate version exist' do
+ let(:migrations_dirname) { 'duplicate_version' }
+
+ it 'raises DuplicateMigrationVersionError' do
+ expect { migration }.to raise_error ClickHouse::MigrationSupport::DuplicateMigrationVersionError
+
+ expect(active_schema_migrations_count).to eq 0
+ end
+ end
+ end
+
+ describe 'performs rollbacks' do
+ subject(:migration) { rollback(target_version, migration_context) }
+
+ before do
+ migrate(nil, migration_context)
+ end
+
+ context 'when migrating back all the way to 0' do
+ let(:target_version) { 0 }
+
+ context 'when down method is present' do
+ let(:migrations_dirname) { 'table_creation_with_down_method' }
+
+ it 'removes migration and performs down method' do
+ expect(table_names).to include('some')
+
+ expect { migration }.to change { active_schema_migrations_count }.from(1).to(0)
+
+ expect(table_names).not_to include('some')
+ expect(schema_migrations).to contain_exactly(a_hash_including(version: '1', active: 0))
+ end
+ end
+
+ context 'when down method is missing' do
+ let(:migrations_dirname) { 'plain_table_creation' }
+
+ it 'removes migration ignoring missing down method' do
+ expect { migration }.to change { active_schema_migrations_count }.from(1).to(0)
+ .and not_change { table_names & %w[some] }.from(%w[some])
+ end
+ end
+ end
+
+ context 'when target_version is incorrect' do
+ let(:target_version) { -1 }
+ let(:migrations_dirname) { 'plain_table_creation' }
+
+ it 'raises UnknownMigrationVersionError' do
+ expect { migration }.to raise_error ClickHouse::MigrationSupport::UnknownMigrationVersionError
+
+ expect(active_schema_migrations_count).to eq 1
+ end
+ end
+ end
+end
diff --git a/spec/components/projects/ml/models_index_component_spec.rb b/spec/components/projects/ml/models_index_component_spec.rb
index c42c94d5d01..b662e8c0a08 100644
--- a/spec/components/projects/ml/models_index_component_spec.rb
+++ b/spec/components/projects/ml/models_index_component_spec.rb
@@ -23,7 +23,7 @@ RSpec.describe Projects::Ml::ModelsIndexComponent, type: :component, feature_cat
end
subject(:component) do
- described_class.new(paginator: paginator)
+ described_class.new(model_count: 5, paginator: paginator)
end
describe 'rendered' do
@@ -43,13 +43,15 @@ RSpec.describe Projects::Ml::ModelsIndexComponent, type: :component, feature_cat
{
'name' => model1.name,
'version' => model1.latest_version.version,
- 'path' => "/#{project.full_path}/-/packages/#{model1.latest_version.package_id}",
+ 'versionPackagePath' => "/#{project.full_path}/-/packages/#{model1.latest_version.package_id}",
+ 'versionPath' => "/#{project.full_path}/-/ml/models/#{model1.id}/versions/#{model1.latest_version.id}",
'versionCount' => 1
},
{
'name' => model2.name,
'version' => nil,
- 'path' => nil,
+ 'versionPackagePath' => nil,
+ 'versionPath' => nil,
'versionCount' => 0
}
],
@@ -58,7 +60,8 @@ RSpec.describe Projects::Ml::ModelsIndexComponent, type: :component, feature_cat
'hasPreviousPage' => false,
'startCursor' => 'abcde',
'endCursor' => 'defgh'
- }
+ },
+ 'modelCount' => 5
})
end
end
diff --git a/spec/components/projects/ml/show_ml_model_component_spec.rb b/spec/components/projects/ml/show_ml_model_component_spec.rb
index 7d08b90791b..ec125851d3d 100644
--- a/spec/components/projects/ml/show_ml_model_component_spec.rb
+++ b/spec/components/projects/ml/show_ml_model_component_spec.rb
@@ -22,7 +22,12 @@ RSpec.describe Projects::Ml::ShowMlModelComponent, type: :component, feature_cat
'model' => {
'id' => model1.id,
'name' => model1.name,
- 'path' => "/#{project.full_path}/-/ml/models/#{model1.id}"
+ 'path' => "/#{project.full_path}/-/ml/models/#{model1.id}",
+ 'description' => 'This is a placeholder for the short description',
+ 'latestVersion' => {
+ 'version' => model1.latest_version.version
+ },
+ 'versionCount' => 1
}
})
end
diff --git a/spec/components/projects/ml/show_ml_model_version_component_spec.rb b/spec/components/projects/ml/show_ml_model_version_component_spec.rb
new file mode 100644
index 00000000000..973d8123c45
--- /dev/null
+++ b/spec/components/projects/ml/show_ml_model_version_component_spec.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+require "spec_helper"
+
+RSpec.describe Projects::Ml::ShowMlModelVersionComponent, type: :component, feature_category: :mlops do
+ let_it_be(:project) { build_stubbed(:project) }
+ let_it_be(:model) { build_stubbed(:ml_models, project: project) }
+ let_it_be(:version) { build_stubbed(:ml_model_versions, model: model) }
+
+ subject(:component) do
+ described_class.new(model_version: version)
+ end
+
+ describe 'rendered' do
+ before do
+ render_inline component
+ end
+
+ it 'renders element with view_model' do
+ element = page.find("#js-mount-show-ml-model-version")
+
+ expect(Gitlab::Json.parse(element['data-view-model'])).to eq({
+ 'modelVersion' => {
+ 'id' => version.id,
+ 'version' => version.version,
+ 'path' => "/#{project.full_path}/-/ml/models/#{model.id}/versions/#{version.id}",
+ 'model' => {
+ 'name' => model.name,
+ 'path' => "/#{project.full_path}/-/ml/models/#{model.id}"
+ }
+ }
+ })
+ end
+ end
+end
diff --git a/spec/controllers/admin/application_settings_controller_spec.rb b/spec/controllers/admin/application_settings_controller_spec.rb
index 60343c822af..8dbdd8db99b 100644
--- a/spec/controllers/admin/application_settings_controller_spec.rb
+++ b/spec/controllers/admin/application_settings_controller_spec.rb
@@ -66,51 +66,69 @@ RSpec.describe Admin::ApplicationSettingsController, :do_not_mock_admin_mode_set
sign_in(admin)
end
+ context 'when there are NO recent ServicePing reports' do
+ it 'return 404' do
+ get :usage_data, format: :json
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+
context 'when there are recent ServicePing reports' do
- it 'attempts to use prerecorded data' do
+ before do
create(:raw_usage_data)
+ end
+ it 'does not trigger ServicePing generation' do
expect(Gitlab::Usage::ServicePingReport).not_to receive(:for)
get :usage_data, format: :json
end
- end
- context 'when there are NO recent ServicePing reports' do
- it 'calculates data on the fly' do
- allow(Gitlab::Usage::ServicePingReport).to receive(:for).and_call_original
+ it 'check cached data if present' do
+ expect(Rails.cache).to receive(:fetch).with(Gitlab::Usage::ServicePingReport::CACHE_KEY).and_return({ test: 1 })
+ expect(::RawUsageData).not_to receive(:for_current_reporting_cycle)
get :usage_data, format: :json
-
- expect(Gitlab::Usage::ServicePingReport).to have_received(:for)
end
- end
- it 'returns HTML data' do
- get :usage_data, format: :html
+ context 'if no cached data available' do
+ before do
+ allow(Rails.cache).to receive(:fetch).and_return(nil)
+ end
- expect(response.body).to start_with('<span')
- expect(response).to have_gitlab_http_status(:ok)
- end
+ it 'returns latest RawUsageData' do
+ expect(::RawUsageData).to receive_message_chain(:for_current_reporting_cycle, :first, :payload)
- it 'returns JSON data' do
- get :usage_data, format: :json
+ get :usage_data, format: :json
+ end
+ end
- body = json_response
- expect(body["version"]).to eq(Gitlab::VERSION)
- expect(body).to include('counts')
- expect(response).to have_gitlab_http_status(:ok)
- end
+ it 'returns HTML data' do
+ get :usage_data, format: :html
- describe 'usage data counter' do
- let(:counter) { Gitlab::UsageDataCounters::ServiceUsageDataCounter }
+ expect(response.body).to start_with('<span')
+ expect(response).to have_gitlab_http_status(:ok)
+ end
- it 'incremented when json generated' do
- expect { get :usage_data, format: :json }.to change { counter.read(:download_payload_click) }.by(1)
+ it 'returns JSON data' do
+ get :usage_data, format: :json
+
+ expect(json_response).to be_present
+ expect(json_response['test']).to include('test')
+ expect(response).to have_gitlab_http_status(:ok)
end
- it 'not incremented when html format requested' do
- expect { get :usage_data }.not_to change { counter.read(:download_payload_click) }
+ describe 'usage data counter' do
+ let(:counter) { Gitlab::UsageDataCounters::ServiceUsageDataCounter }
+
+ it 'incremented when json generated' do
+ expect { get :usage_data, format: :json }.to change { counter.read(:download_payload_click) }.by(1)
+ end
+
+ it 'not incremented when html format requested' do
+ expect { get :usage_data }.not_to change { counter.read(:download_payload_click) }
+ end
end
end
end
@@ -524,35 +542,37 @@ RSpec.describe Admin::ApplicationSettingsController, :do_not_mock_admin_mode_set
end
end
- describe 'GET #service_usage_data', feature_category: :service_ping do
+ describe 'GET #metrics_and_profiling', feature_category: :service_ping do
before do
stub_usage_data_connections
stub_database_flavor_check
sign_in(admin)
end
- it 'assigns truthy value if there are recent ServicePing reports in database' do
+ it 'assigns service_ping_data if there are recent ServicePing reports in database' do
create(:raw_usage_data)
- get :service_usage_data, format: :html
+ get :metrics_and_profiling, format: :html
- expect(assigns(:service_ping_data_present)).to be_truthy
+ expect(assigns(:service_ping_data)).to be_present
expect(response).to have_gitlab_http_status(:ok)
end
- it 'assigns truthy value if there are recent ServicePing reports in cache', :use_clean_rails_memory_store_caching do
- Rails.cache.write('usage_data', true)
+ it 'assigns service_ping_data if there are recent ServicePing reports in cache', :use_clean_rails_memory_store_caching do
+ create(:raw_usage_data)
+ cached_data = { testKey: "testValue" }
+ Rails.cache.write('usage_data', cached_data)
- get :service_usage_data, format: :html
+ get :metrics_and_profiling, format: :html
- expect(assigns(:service_ping_data_present)).to be_truthy
+ expect(assigns(:service_ping_data)).to eq(cached_data)
expect(response).to have_gitlab_http_status(:ok)
end
- it 'assigns falsey value if there are NO recent ServicePing reports' do
- get :service_usage_data, format: :html
+ it 'does not assign service_ping_data value if there are NO recent ServicePing reports' do
+ get :metrics_and_profiling, format: :html
- expect(assigns(:service_ping_data_present)).to be_falsey
+ expect(assigns(:service_ping_data)).not_to be_present
expect(response).to have_gitlab_http_status(:ok)
end
end
diff --git a/spec/controllers/autocomplete_controller_spec.rb b/spec/controllers/autocomplete_controller_spec.rb
index a66cb4364d7..cbe0319a78d 100644
--- a/spec/controllers/autocomplete_controller_spec.rb
+++ b/spec/controllers/autocomplete_controller_spec.rb
@@ -454,77 +454,85 @@ RSpec.describe AutocompleteController do
end
end
- context 'Get merge_request_target_branches', feature_category: :code_review_workflow do
- let!(:merge_request) { create(:merge_request, source_project: project, target_branch: 'feature') }
+ context 'GET branches', feature_category: :code_review_workflow do
+ let_it_be(:merge_request) do
+ create(:merge_request, source_project: project,
+ source_branch: 'test_source_branch', target_branch: 'test_target_branch')
+ end
- context 'anonymous user' do
- it 'returns empty json' do
- get :merge_request_target_branches, params: { project_id: project.id }
+ shared_examples 'Get merge_request_{}_branches' do |path, expected_result|
+ context 'anonymous user' do
+ it 'returns empty json' do
+ get path, params: { project_id: project.id }
- expect(response).to have_gitlab_http_status(:ok)
- expect(json_response).to be_empty
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response).to be_empty
+ end
end
- end
- context 'user without any accessible merge requests' do
- it 'returns empty json' do
- sign_in(create(:user))
+ context 'user without any accessible merge requests' do
+ it 'returns empty json' do
+ sign_in(create(:user))
- get :merge_request_target_branches, params: { project_id: project.id }
+ get path, params: { project_id: project.id }
- expect(response).to have_gitlab_http_status(:ok)
- expect(json_response).to be_empty
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response).to be_empty
+ end
end
- end
- context 'user with an accessible merge request but no scope' do
- where(
- params: [
- {},
- { group_id: ' ' },
- { project_id: ' ' },
- { group_id: ' ', project_id: ' ' }
- ]
- )
-
- with_them do
- it 'returns an error' do
- sign_in(user)
+ context 'user with an accessible merge request but no scope' do
+ where(
+ params: [
+ {},
+ { group_id: ' ' },
+ { project_id: ' ' },
+ { group_id: ' ', project_id: ' ' }
+ ]
+ )
+
+ with_them do
+ it 'returns an error' do
+ sign_in(user)
- get :merge_request_target_branches, params: params
+ get path, params: params
- expect(response).to have_gitlab_http_status(:bad_request)
- expect(json_response).to eq({ 'error' => 'At least one of group_id or project_id must be specified' })
+ expect(response).to have_gitlab_http_status(:bad_request)
+ expect(json_response).to eq({ 'error' => 'At least one of group_id or project_id must be specified' })
+ end
end
end
- end
- context 'user with an accessible merge request by project' do
- it 'returns json' do
- sign_in(user)
+ context 'user with an accessible merge request by project' do
+ it 'returns json' do
+ sign_in(user)
- get :merge_request_target_branches, params: { project_id: project.id }
+ get path, params: { project_id: project.id }
- expect(response).to have_gitlab_http_status(:ok)
- expect(json_response).to contain_exactly({ 'title' => 'feature' })
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response).to contain_exactly(expected_result)
+ end
end
- end
- context 'user with an accessible merge request by group' do
- let(:group) { create(:group) }
- let(:project) { create(:project, namespace: group) }
- let(:user) { create(:user) }
+ context 'user with an accessible merge request by group' do
+ let(:group) { create(:group) }
+ let(:user) { create(:user) }
- it 'returns json' do
- group.add_owner(user)
+ it 'returns json' do
+ project.update!(namespace: group)
+ group.add_owner(user)
- sign_in(user)
+ sign_in(user)
- get :merge_request_target_branches, params: { group_id: group.id }
+ get path, params: { group_id: group.id }
- expect(response).to have_gitlab_http_status(:ok)
- expect(json_response).to contain_exactly({ 'title' => 'feature' })
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response).to contain_exactly(expected_result)
+ end
end
end
+
+ it_behaves_like 'Get merge_request_{}_branches', :merge_request_target_branches, { 'title' => 'test_target_branch' }
+ it_behaves_like 'Get merge_request_{}_branches', :merge_request_source_branches, { 'title' => 'test_source_branch' }
end
end
diff --git a/spec/controllers/groups/settings/applications_controller_spec.rb b/spec/controllers/groups/settings/applications_controller_spec.rb
index c398fd044c2..aa50ef9a92c 100644
--- a/spec/controllers/groups/settings/applications_controller_spec.rb
+++ b/spec/controllers/groups/settings/applications_controller_spec.rb
@@ -23,17 +23,55 @@ RSpec.describe Groups::Settings::ApplicationsController do
expect(response).to render_template :index
expect(assigns[:scopes]).to be_kind_of(Doorkeeper::OAuth::Scopes)
end
- end
- context 'when user is not owner' do
- before do
- group.add_maintainer(user)
+ context 'when admin mode is enabled' do
+ let!(:user) { create(:user, :admin) }
+
+ before do
+ Gitlab::Session.with_session(controller.session) do
+ controller.current_user_mode.request_admin_mode!
+ controller.current_user_mode.enable_admin_mode!(password: user.password)
+ end
+ end
+
+ it 'renders the applications page' do
+ get :index, params: { group_id: group }
+
+ expect(response).to render_template :index
+ expect(assigns[:scopes]).to be_kind_of(Doorkeeper::OAuth::Scopes)
+ end
end
+ end
- it 'renders a 404' do
- get :index, params: { group_id: group }
+ %w[guest reporter developer maintainer].each do |role|
+ context "when user is a #{role}" do
+ before do
+ group.send("add_#{role}", user)
+ end
+
+ it 'renders a 404' do
+ get :index, params: { group_id: group }
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+
+ context "when admin mode is enabled for the admin user who is a #{role} of a group" do
+ let!(:user) { create(:user, :admin) }
+
+ before do
+ Gitlab::Session.with_session(controller.session) do
+ controller.current_user_mode.request_admin_mode!
+ controller.current_user_mode.enable_admin_mode!(password: user.password)
+ end
+ end
+
+ it 'renders the applications page' do
+ get :index, params: { group_id: group }
- expect(response).to have_gitlab_http_status(:not_found)
+ expect(response).to render_template :index
+ expect(assigns[:scopes]).to be_kind_of(Doorkeeper::OAuth::Scopes)
+ end
+ end
end
end
end
@@ -44,23 +82,61 @@ RSpec.describe Groups::Settings::ApplicationsController do
group.add_owner(user)
end
- it 'renders the application form' do
+ it 'renders the edit application page' do
get :edit, params: { group_id: group, id: application.id }
expect(response).to render_template :edit
expect(assigns[:scopes]).to be_kind_of(Doorkeeper::OAuth::Scopes)
end
- end
- context 'when user is not owner' do
- before do
- group.add_maintainer(user)
+ context 'when admin mode is enabled' do
+ let!(:user) { create(:user, :admin) }
+
+ before do
+ Gitlab::Session.with_session(controller.session) do
+ controller.current_user_mode.request_admin_mode!
+ controller.current_user_mode.enable_admin_mode!(password: user.password)
+ end
+ end
+
+ it 'renders the edit application page' do
+ get :edit, params: { group_id: group, id: application.id }
+
+ expect(response).to render_template :edit
+ expect(assigns[:scopes]).to be_kind_of(Doorkeeper::OAuth::Scopes)
+ end
end
+ end
- it 'renders a 404' do
- get :edit, params: { group_id: group, id: application.id }
+ %w[guest reporter developer maintainer].each do |role|
+ context "when user is a #{role}" do
+ before do
+ group.send("add_#{role}", user)
+ end
+
+ it 'renders a 404' do
+ get :edit, params: { group_id: group, id: application.id }
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+
+ context "when admin mode is enabled for the admin user who is a #{role} of a group" do
+ let!(:user) { create(:user, :admin) }
- expect(response).to have_gitlab_http_status(:not_found)
+ before do
+ Gitlab::Session.with_session(controller.session) do
+ controller.current_user_mode.request_admin_mode!
+ controller.current_user_mode.enable_admin_mode!(password: user.password)
+ end
+ end
+
+ it 'renders the edit application page' do
+ get :edit, params: { group_id: group, id: application.id }
+
+ expect(response).to render_template :edit
+ expect(assigns[:scopes]).to be_kind_of(Doorkeeper::OAuth::Scopes)
+ end
+ end
end
end
end
@@ -121,19 +197,71 @@ RSpec.describe Groups::Settings::ApplicationsController do
expect(response).to render_template :index
end
end
- end
- context 'when user is not owner' do
- before do
- group.add_maintainer(user)
+ context 'when admin mode is enabled' do
+ let!(:user) { create(:user, :admin) }
+
+ before do
+ Gitlab::Session.with_session(controller.session) do
+ controller.current_user_mode.request_admin_mode!
+ controller.current_user_mode.enable_admin_mode!(password: user.password)
+ end
+ end
+
+ it 'creates the application' do
+ create_params = attributes_for(:application, trusted: false, confidential: false, scopes: ['api'])
+
+ expect do
+ post :create, params: { group_id: group, doorkeeper_application: create_params }
+ end.to change { Doorkeeper::Application.count }.by(1)
+
+ application = Doorkeeper::Application.last
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to render_template :show
+ expect(application).to have_attributes(create_params.except(:uid, :owner_type))
+ end
end
+ end
+
+ %w[guest reporter developer maintainer].each do |role|
+ context "when user is a #{role}" do
+ let(:create_params) { attributes_for(:application, trusted: true, confidential: false, scopes: ['api']) }
+
+ before do
+ group.send("add_#{role}", user)
+ end
+
+ it 'renders a 404' do
+ post :create, params: { group_id: group, doorkeeper_application: create_params }
- it 'renders a 404' do
- create_params = attributes_for(:application, trusted: true, confidential: false, scopes: ['api'])
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+
+ context "when admin mode is enabled for the admin user who is a #{role} of a group" do
+ let!(:user) { create(:user, :admin) }
+
+ before do
+ Gitlab::Session.with_session(controller.session) do
+ controller.current_user_mode.request_admin_mode!
+ controller.current_user_mode.enable_admin_mode!(password: user.password)
+ end
+ end
+
+ it 'creates the application' do
+ create_params = attributes_for(:application, trusted: false, confidential: false, scopes: ['api'])
- post :create, params: { group_id: group, doorkeeper_application: create_params }
+ expect do
+ post :create, params: { group_id: group, doorkeeper_application: create_params }
+ end.to change { Doorkeeper::Application.count }.by(1)
- expect(response).to have_gitlab_http_status(:not_found)
+ application = Doorkeeper::Application.last
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to render_template :show
+ expect(application).to have_attributes(create_params.except(:uid, :owner_type))
+ end
+ end
end
end
end
@@ -162,6 +290,26 @@ RSpec.describe Groups::Settings::ApplicationsController do
expect(json_response['secret']).not_to be_nil
end
+ context 'when admin mode is enabled' do
+ let!(:user) { create(:user, :admin) }
+
+ before do
+ Gitlab::Session.with_session(controller.session) do
+ controller.current_user_mode.request_admin_mode!
+ controller.current_user_mode.enable_admin_mode!(password: user.password)
+ end
+ end
+
+ it { is_expected.to have_gitlab_http_status(:ok) }
+ it { expect { subject }.to change { application.reload.secret } }
+
+ it 'returns the secret in json format' do
+ subject
+
+ expect(json_response['secret']).not_to be_nil
+ end
+ end
+
context 'when renew fails' do
before do
allow_next_found_instance_of(Doorkeeper::Application) do |application|
@@ -174,21 +322,42 @@ RSpec.describe Groups::Settings::ApplicationsController do
end
end
- context 'when user is not owner' do
- before do
- group.add_maintainer(user)
- end
+ %w[guest reporter developer maintainer].each do |role|
+ context "when user is a #{role}" do
+ let(:oauth_params) do
+ {
+ group_id: group,
+ id: application.id
+ }
+ end
- let(:oauth_params) do
- {
- group_id: group,
- id: application.id
- }
- end
+ before do
+ group.send("add_#{role}", user)
+ end
- it 'renders a 404' do
- put :renew, params: oauth_params
- expect(response).to have_gitlab_http_status(:not_found)
+ it 'renders a 404' do
+ put :renew, params: oauth_params
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+
+ context "when admin mode is enabled for the admin user who is a #{role} of a group" do
+ let!(:user) { create(:user, :admin) }
+
+ before do
+ Gitlab::Session.with_session(controller.session) do
+ controller.current_user_mode.request_admin_mode!
+ controller.current_user_mode.enable_admin_mode!(password: user.password)
+ end
+ end
+
+ it 'returns the secret in json format' do
+ put :renew, params: oauth_params
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response['secret']).not_to be_nil
+ end
+ end
end
end
end
@@ -230,19 +399,67 @@ RSpec.describe Groups::Settings::ApplicationsController do
expect(application).to be_confidential
end
end
- end
- context 'when user is not owner' do
- before do
- group.add_maintainer(user)
+ context 'when admin mode is enabled' do
+ let!(:user) { create(:user, :admin) }
+
+ before do
+ Gitlab::Session.with_session(controller.session) do
+ controller.current_user_mode.request_admin_mode!
+ controller.current_user_mode.enable_admin_mode!(password: user.password)
+ end
+ end
+
+ it 'updates the application' do
+ doorkeeper_params = { redirect_uri: 'http://example.com/', trusted: true, confidential: false }
+
+ patch :update, params: { group_id: group, id: application.id, doorkeeper_application: doorkeeper_params }
+
+ application.reload
+
+ expect(response).to redirect_to(group_settings_application_path(group, application))
+ expect(application)
+ .to have_attributes(redirect_uri: 'http://example.com/', trusted: false, confidential: false)
+ end
end
+ end
- it 'renders a 404' do
- doorkeeper_params = { redirect_uri: 'http://example.com/', trusted: true, confidential: false }
+ %w[guest reporter developer maintainer].each do |role|
+ context "when user is a #{role}" do
+ before do
+ group.send("add_#{role}", user)
+ end
- patch :update, params: { group_id: group, id: application.id, doorkeeper_application: doorkeeper_params }
+ it 'renders a 404' do
+ doorkeeper_params = { redirect_uri: 'http://example.com/', trusted: true, confidential: false }
+
+ patch :update, params: { group_id: group, id: application.id, doorkeeper_application: doorkeeper_params }
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
- expect(response).to have_gitlab_http_status(:not_found)
+ context "when admin mode is enabled for the admin user who is a #{role} of a group" do
+ let!(:user) { create(:user, :admin) }
+
+ before do
+ Gitlab::Session.with_session(controller.session) do
+ controller.current_user_mode.request_admin_mode!
+ controller.current_user_mode.enable_admin_mode!(password: user.password)
+ end
+ end
+
+ it 'updates the application' do
+ doorkeeper_params = { redirect_uri: 'http://example.com/', trusted: true, confidential: false }
+
+ patch :update, params: { group_id: group, id: application.id, doorkeeper_application: doorkeeper_params }
+
+ application.reload
+
+ expect(response).to redirect_to(group_settings_application_path(group, application))
+ expect(application)
+ .to have_attributes(redirect_uri: 'http://example.com/', trusted: false, confidential: false)
+ end
+ end
end
end
end
@@ -259,17 +476,55 @@ RSpec.describe Groups::Settings::ApplicationsController do
expect(Doorkeeper::Application.exists?(application.id)).to be_falsy
expect(response).to redirect_to(group_settings_applications_url(group))
end
- end
- context 'when user is not owner' do
- before do
- group.add_maintainer(user)
+ context 'when admin mode is enabled' do
+ let!(:user) { create(:user, :admin) }
+
+ before do
+ Gitlab::Session.with_session(controller.session) do
+ controller.current_user_mode.request_admin_mode!
+ controller.current_user_mode.enable_admin_mode!(password: user.password)
+ end
+ end
+
+ it 'deletes the application' do
+ delete :destroy, params: { group_id: group, id: application.id }
+
+ expect(Doorkeeper::Application.exists?(application.id)).to be_falsy
+ expect(response).to redirect_to(group_settings_applications_url(group))
+ end
end
+ end
- it 'renders a 404' do
- delete :destroy, params: { group_id: group, id: application.id }
+ %w[guest reporter developer maintainer].each do |role|
+ context "when user is a #{role}" do
+ before do
+ group.send("add_#{role}", user)
+ end
+
+ it 'renders a 404' do
+ delete :destroy, params: { group_id: group, id: application.id }
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+
+ context "when admin mode is enabled for the admin user who is a #{role} of a group" do
+ let!(:user) { create(:user, :admin) }
- expect(response).to have_gitlab_http_status(:not_found)
+ before do
+ Gitlab::Session.with_session(controller.session) do
+ controller.current_user_mode.request_admin_mode!
+ controller.current_user_mode.enable_admin_mode!(password: user.password)
+ end
+ end
+
+ it 'deletes the application' do
+ delete :destroy, params: { group_id: group, id: application.id }
+
+ expect(Doorkeeper::Application.exists?(application.id)).to be_falsy
+ expect(response).to redirect_to(group_settings_applications_url(group))
+ end
+ end
end
end
end
diff --git a/spec/controllers/import/bulk_imports_controller_spec.rb b/spec/controllers/import/bulk_imports_controller_spec.rb
index c5e5aa03669..57c723829e3 100644
--- a/spec/controllers/import/bulk_imports_controller_spec.rb
+++ b/spec/controllers/import/bulk_imports_controller_spec.rb
@@ -300,6 +300,33 @@ RSpec.describe Import::BulkImportsController, feature_category: :importers do
end
end
+ describe 'GET details' do
+ subject(:request) { get :details }
+
+ context 'when bulk_import_details_page feature flag is enabled' do
+ before do
+ stub_feature_flags(bulk_import_details_page: true)
+ request
+ end
+
+ it 'responds with a 200 and shows the template', :aggregate_failures do
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to render_template(:details)
+ end
+ end
+
+ context 'when bulk_import_details_page feature flag is disabled' do
+ before do
+ stub_feature_flags(bulk_import_details_page: false)
+ request
+ end
+
+ it 'responds with a 404' do
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+ end
+
describe 'GET realtime_changes' do
let_it_be(:bulk_import) { create(:bulk_import, :created, user: user) }
diff --git a/spec/controllers/oauth/jira_dvcs/authorizations_controller_spec.rb b/spec/controllers/oauth/jira_dvcs/authorizations_controller_spec.rb
deleted file mode 100644
index 3d271a22f27..00000000000
--- a/spec/controllers/oauth/jira_dvcs/authorizations_controller_spec.rb
+++ /dev/null
@@ -1,71 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Oauth::JiraDvcs::AuthorizationsController, feature_category: :integrations do
- let_it_be(:application) { create(:oauth_application, redirect_uri: 'https://example.com/callback') }
-
- describe 'GET new' do
- it 'redirects to OAuth authorization with correct params' do
- get :new, params: { client_id: application.uid, scope: 'foo', redirect_uri: 'https://example.com/callback' }
-
- expect(response).to redirect_to(oauth_authorization_url(
- client_id: application.uid,
- response_type: 'code',
- scope: 'foo',
- redirect_uri: oauth_jira_dvcs_callback_url))
- end
-
- it 'replaces the GitHub "repo" scope with "api"' do
- get :new, params: { client_id: application.uid, scope: 'repo', redirect_uri: 'https://example.com/callback' }
-
- expect(response).to redirect_to(oauth_authorization_url(
- client_id: application.uid,
- response_type: 'code',
- scope: 'api',
- redirect_uri: oauth_jira_dvcs_callback_url))
- end
-
- it 'returns 404 with an invalid client' do
- get :new, params: { client_id: 'client-123', scope: 'foo', redirect_uri: 'https://example.com/callback' }
-
- expect(response).to have_gitlab_http_status(:not_found)
- end
-
- it 'returns 403 with an incorrect redirect_uri' do
- get :new, params: { client_id: application.uid, scope: 'foo', redirect_uri: 'http://unsafe-website.com/callback' }
-
- expect(response).to have_gitlab_http_status(:forbidden)
- end
- end
-
- describe 'GET callback' do
- it 'redirects to redirect_uri on session with code param' do
- session['redirect_uri'] = 'http://example.com'
-
- get :callback, params: { code: 'hash-123' }
-
- expect(response).to redirect_to('http://example.com?code=hash-123')
- end
-
- it 'redirects to redirect_uri on session with code param preserving existing query' do
- session['redirect_uri'] = 'http://example.com?foo=bar'
-
- get :callback, params: { code: 'hash-123' }
-
- expect(response).to redirect_to('http://example.com?foo=bar&code=hash-123')
- end
- end
-
- describe 'POST access_token' do
- it 'returns oauth params in a format Jira expects' do
- expect_any_instance_of(Doorkeeper::Request::AuthorizationCode).to receive(:authorize) do
- double(status: :ok, body: { 'access_token' => 'fake-123', 'scope' => 'foo', 'token_type' => 'bar' })
- end
-
- post :access_token, params: { code: 'code-123', client_id: application.uid, client_secret: 'secret-123' }
-
- expect(response.body).to eq('access_token=fake-123&scope=foo&token_type=bar')
- end
- end
-end
diff --git a/spec/controllers/profiles/notifications_controller_spec.rb b/spec/controllers/profiles/notifications_controller_spec.rb
index 22c0a62a6a1..ef49eb911ba 100644
--- a/spec/controllers/profiles/notifications_controller_spec.rb
+++ b/spec/controllers/profiles/notifications_controller_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Profiles::NotificationsController do
+RSpec.describe Profiles::NotificationsController, feature_category: :team_planning do
let(:user) do
create(:user) do |user|
user.emails.create!(email: 'original@example.com', confirmed_at: Time.current)
diff --git a/spec/controllers/profiles_controller_spec.rb b/spec/controllers/profiles_controller_spec.rb
index 2bcb47f97ab..4f350ddf1ef 100644
--- a/spec/controllers/profiles_controller_spec.rb
+++ b/spec/controllers/profiles_controller_spec.rb
@@ -128,6 +128,16 @@ RSpec.describe ProfilesController, :request_store do
expect(user.reload.discord).to eq(discord_user_id)
expect(response).to have_gitlab_http_status(:found)
end
+
+ it 'allows updating user specified mastodon username', :aggregate_failures do
+ mastodon_username = '@robin@example.com'
+ sign_in(user)
+
+ put :update, params: { user: { mastodon: mastodon_username } }
+
+ expect(user.reload.mastodon).to eq(mastodon_username)
+ expect(response).to have_gitlab_http_status(:found)
+ end
end
describe 'GET audit_log' do
diff --git a/spec/controllers/projects/artifacts_controller_spec.rb b/spec/controllers/projects/artifacts_controller_spec.rb
index 31e6d6ae5e6..a0548e847a0 100644
--- a/spec/controllers/projects/artifacts_controller_spec.rb
+++ b/spec/controllers/projects/artifacts_controller_spec.rb
@@ -324,12 +324,32 @@ RSpec.describe Projects::ArtifactsController, feature_category: :build_artifacts
end
context 'when the file exists' do
- it 'renders the file view' do
- path = 'ci_artifacts.txt'
+ context 'when the external redirect page is enabled' do
+ before do
+ stub_application_setting(enable_artifact_external_redirect_warning_page: true)
+ end
+
+ it 'redirects to the user-generated content warning page' do
+ path = 'ci_artifacts.txt'
- get :file, params: { namespace_id: project.namespace, project_id: project, job_id: job, path: path }
+ get :file, params: { namespace_id: project.namespace, project_id: project, job_id: job, path: path }
+
+ expect(response).to redirect_to(external_file_project_job_artifacts_path(project, job, path: path))
+ end
+ end
- expect(response).to redirect_to(external_file_project_job_artifacts_path(project, job, path: path))
+ context 'when the external redirect page is disabled' do
+ before do
+ stub_application_setting(enable_artifact_external_redirect_warning_page: false)
+ end
+
+ it 'renders the file view' do
+ path = 'ci_artifacts.txt'
+
+ get :file, params: { namespace_id: project.namespace, project_id: project, job_id: job, path: path }
+
+ expect(response).to have_gitlab_http_status(:found)
+ end
end
end
end
diff --git a/spec/controllers/projects/blob_controller_spec.rb b/spec/controllers/projects/blob_controller_spec.rb
index 49c1935c4a3..7811ef3bade 100644
--- a/spec/controllers/projects/blob_controller_spec.rb
+++ b/spec/controllers/projects/blob_controller_spec.rb
@@ -23,18 +23,12 @@ RSpec.describe Projects::BlobController, feature_category: :source_code_manageme
render_views
context 'with file path' do
+ include_context 'with ambiguous refs for controllers'
+
before do
- expect(::Gitlab::GitalyClient).to receive(:allow_ref_name_caching).and_call_original
- project.repository.add_tag(project.creator, 'ambiguous_ref', RepoHelpers.sample_commit.id)
- project.repository.add_branch(project.creator, 'ambiguous_ref', RepoHelpers.another_sample_commit.id)
request
end
- after do
- project.repository.rm_tag(project.creator, 'ambiguous_ref')
- project.repository.rm_branch(project.creator, 'ambiguous_ref')
- end
-
context 'when the ref is ambiguous' do
let(:ref) { 'ambiguous_ref' }
let(:path) { 'README.md' }
@@ -43,6 +37,8 @@ RSpec.describe Projects::BlobController, feature_category: :source_code_manageme
context 'and the redirect_with_ref_type flag is disabled' do
let(:redirect_with_ref_type) { false }
+ it_behaves_like '#set_is_ambiguous_ref when ref is ambiguous'
+
context 'and explicitly requesting a branch' do
let(:ref_type) { 'heads' }
@@ -61,16 +57,22 @@ RSpec.describe Projects::BlobController, feature_category: :source_code_manageme
end
context 'and the redirect_with_ref_type flag is enabled' do
- context 'when the ref_type is nil' do
- let(:ref_type) { nil }
+ let(:ref_type) { nil }
- it 'redirects to the tag' do
- expect(response).to redirect_to(project_blob_path(project, id, ref_type: 'tags'))
- end
+ it 'redirects to the tag' do
+ expect(response).to redirect_to(project_blob_path(project, id, ref_type: 'tags'))
end
end
end
+ describe '#set_is_ambiguous_ref with no ambiguous ref' do
+ let(:id) { 'master/invalid-path.rb' }
+ let(:redirect_with_ref_type) { false }
+ let(:ambiguous_ref_modal) { true }
+
+ it_behaves_like '#set_is_ambiguous_ref when ref is not ambiguous'
+ end
+
context "valid branch, valid file" do
let(:id) { 'master/README.md' }
diff --git a/spec/controllers/projects/group_links_controller_spec.rb b/spec/controllers/projects/group_links_controller_spec.rb
index 2075dd3e7a7..4510e9e646e 100644
--- a/spec/controllers/projects/group_links_controller_spec.rb
+++ b/spec/controllers/projects/group_links_controller_spec.rb
@@ -30,24 +30,29 @@ RSpec.describe Projects::GroupLinksController, feature_category: :system_access
end
let(:expiry_date) { 1.month.from_now.to_date }
+ let(:group_access) { Gitlab::Access::GUEST }
- before do
- travel_to Time.now.utc.beginning_of_day
-
+ subject(:update_link) do
put(
:update,
params: {
namespace_id: project.namespace.to_param,
project_id: project.to_param,
id: link.id,
- group_link: { group_access: Gitlab::Access::GUEST, expires_at: expiry_date }
+ group_link: { group_access: group_access, expires_at: expiry_date }
},
format: :json
)
end
+ before do
+ travel_to Time.now.utc.beginning_of_day
+ end
+
context 'when `expires_at` is set' do
it 'returns correct json response' do
+ update_link
+
expect(json_response).to eq({ "expires_in" => controller.helpers.time_ago_with_tooltip(expiry_date), "expires_soon" => false })
end
end
@@ -56,27 +61,41 @@ RSpec.describe Projects::GroupLinksController, feature_category: :system_access
let(:expiry_date) { nil }
it 'returns empty json response' do
+ update_link
+
expect(json_response).to be_empty
end
end
+
+ it "returns an error when link is not updated" do
+ allow(::Projects::GroupLinks::UpdateService).to receive_message_chain(:new, :execute)
+ .and_return(ServiceResponse.error(message: '404 Not Found', reason: :not_found))
+
+ update_link
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ expect(json_response['message']).to eq('404 Not Found')
+ end
end
describe '#destroy' do
let(:group_owner) { create(:user) }
+ let(:group_access) { Gitlab::Access::DEVELOPER }
+ let(:format) { :html }
- let(:link) do
- create(:project_group_link, project: project, group: group, group_access: Gitlab::Access::DEVELOPER)
+ let!(:link) do
+ create(:project_group_link, project: project, group: group, group_access: group_access)
end
subject(:destroy_link) do
post(:destroy, params: { namespace_id: project.namespace.to_param,
project_id: project.to_param,
- id: link.id })
+ id: link.id }, format: format)
end
shared_examples 'success response' do
it 'deletes the project group link' do
- destroy_link
+ expect { destroy_link }.to change { project.reload.project_group_links.count }
expect(response).to redirect_to(project_project_members_path(project))
expect(response).to have_gitlab_http_status(:found)
@@ -119,6 +138,27 @@ RSpec.describe Projects::GroupLinksController, feature_category: :system_access
end
it_behaves_like 'success response'
+
+ it "returns an error when link is not destroyed" do
+ allow(::Projects::GroupLinks::DestroyService).to receive_message_chain(:new, :execute)
+ .and_return(ServiceResponse.error(message: 'The error message'))
+
+ expect { destroy_link }.not_to change { project.reload.project_group_links.count }
+ expect(flash[:alert]).to eq('The project-group link could not be removed.')
+ end
+
+ context 'when format is js' do
+ let(:format) { :js }
+
+ it "returns an error when link is not destroyed" do
+ allow(::Projects::GroupLinks::DestroyService).to receive_message_chain(:new, :execute)
+ .and_return(ServiceResponse.error(message: '404 Not Found', reason: :not_found))
+
+ expect { destroy_link }.not_to change { project.reload.project_group_links.count }
+ expect(response).to have_gitlab_http_status(:not_found)
+ expect(json_response['message']).to eq('404 Not Found')
+ end
+ end
end
context 'when user is not a project maintainer' do
diff --git a/spec/controllers/projects/jobs_controller_spec.rb b/spec/controllers/projects/jobs_controller_spec.rb
index 9851153bd39..58da1d37904 100644
--- a/spec/controllers/projects/jobs_controller_spec.rb
+++ b/spec/controllers/projects/jobs_controller_spec.rb
@@ -621,6 +621,64 @@ RSpec.describe Projects::JobsController, :clean_gitlab_redis_shared_state, featu
end
end
+ describe 'GET test_report_summary.json' do
+ let_it_be(:build) { create(:ci_build, :success, :test_reports, project: project) }
+
+ before do
+ sign_in(user)
+ end
+
+ context 'when the user has access' do
+ let(:user) { developer }
+
+ context 'when the summary has been generated' do
+ let!(:report_result) { create(:ci_build_report_result, build: build, project: project) }
+
+ before do
+ get_test_report_summary
+ end
+
+ it 'returns the summary as json' do
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to match_response_schema('job/test_report_summary')
+ end
+ end
+
+ context 'when the summary has not been generated' do
+ before do
+ get_test_report_summary
+ end
+
+ it 'returns a 404 response' do
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+ end
+
+ context 'when the user does not have access' do
+ let(:user) { guest }
+
+ before do
+ project.update!(public_builds: false)
+ get_test_report_summary
+ end
+
+ it 'returns not_found status' do
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+
+ def get_test_report_summary
+ get :test_report_summary,
+ params: {
+ namespace_id: project.namespace,
+ project_id: project,
+ id: build.id
+ },
+ format: :json
+ end
+ end
+
describe 'GET trace.json' do
before do
get_trace
diff --git a/spec/controllers/projects/merge_requests/drafts_controller_spec.rb b/spec/controllers/projects/merge_requests/drafts_controller_spec.rb
index 68fbeb00b67..505f9f5b19b 100644
--- a/spec/controllers/projects/merge_requests/drafts_controller_spec.rb
+++ b/spec/controllers/projects/merge_requests/drafts_controller_spec.rb
@@ -4,10 +4,10 @@ require 'spec_helper'
RSpec.describe Projects::MergeRequests::DraftsController, feature_category: :code_review_workflow do
include RepoHelpers
- let(:project) { create(:project, :repository) }
- let(:merge_request) { create(:merge_request_with_diffs, target_project: project, source_project: project, author: create(:user)) }
- let(:user) { project.first_owner }
- let(:user2) { create(:user) }
+ let_it_be(:project) { create(:project, :repository) }
+ let_it_be_with_reload(:merge_request) { create(:merge_request_with_diffs, target_project: project, source_project: project, author: create(:user)) }
+ let(:user) { project.first_owner }
+ let_it_be(:user2) { create(:user) }
let(:params) do
{
@@ -18,6 +18,8 @@ RSpec.describe Projects::MergeRequests::DraftsController, feature_category: :cod
end
before do
+ create(:merge_request_reviewer, merge_request: merge_request, reviewer: user)
+
sign_in(user)
stub_licensed_features(multiple_merge_request_assignees: true)
stub_commonmark_sourcepos_disabled
@@ -216,9 +218,12 @@ RSpec.describe Projects::MergeRequests::DraftsController, feature_category: :cod
end
context 'without permissions' do
+ before_all do
+ project.add_developer(user2)
+ end
+
before do
sign_in(user2)
- project.add_developer(user2)
end
it 'does not allow editing draft note belonging to someone else' do
@@ -282,7 +287,7 @@ RSpec.describe Projects::MergeRequests::DraftsController, feature_category: :cod
end
context 'when note belongs to someone else' do
- before do
+ before_all do
project.add_developer(user2)
end
@@ -465,6 +470,24 @@ RSpec.describe Projects::MergeRequests::DraftsController, feature_category: :cod
end
end
+ context 'reviewer state' do
+ before do
+ create(:draft_note, merge_request: merge_request, author: user)
+ end
+
+ it 'updates reviewers state' do
+ post :publish, params: params.merge!(reviewer_state: 'requested_changes')
+
+ expect(merge_request.merge_request_reviewers.reload[0].state).to eq('requested_changes')
+ end
+
+ it 'approves merge request' do
+ post :publish, params: params.merge!(reviewer_state: 'approved')
+
+ expect(merge_request.approvals.reload.size).to eq(1)
+ end
+ end
+
context 'approve merge request' do
before do
allow(Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter)
@@ -517,9 +540,12 @@ RSpec.describe Projects::MergeRequests::DraftsController, feature_category: :cod
end
context 'without permissions' do
+ before_all do
+ project.add_developer(user2)
+ end
+
before do
sign_in(user2)
- project.add_developer(user2)
end
it 'does not allow destroying a draft note belonging to someone else' do
@@ -562,9 +588,12 @@ RSpec.describe Projects::MergeRequests::DraftsController, feature_category: :cod
end
context 'without permissions' do
+ before_all do
+ project.add_developer(user2)
+ end
+
before do
sign_in(user2)
- project.add_developer(user2)
end
it 'does not destroys a draft note belonging to someone else' do
diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb
index 92bbffdfde5..539c6d17e0e 100644
--- a/spec/controllers/projects/merge_requests_controller_spec.rb
+++ b/spec/controllers/projects/merge_requests_controller_spec.rb
@@ -2305,68 +2305,139 @@ RSpec.describe Projects::MergeRequestsController, feature_category: :code_review
end
context 'highlight preloading' do
- context 'with commit diff notes' do
- let!(:commit_diff_note) do
- create(:diff_note_on_commit, project: merge_request.project)
+ context 'when only_highlight_discussions_requested is false' do
+ before do
+ stub_feature_flags(only_highlight_discussions_requested: false)
end
- it 'preloads notes diffs highlights' do
- expect_next_instance_of(Gitlab::DiscussionsDiff::FileCollection) do |collection|
- note_diff_file = commit_diff_note.note_diff_file
+ context 'with commit diff notes' do
+ let!(:first_commit_diff_note) do
+ create(:diff_note_on_commit, project: merge_request.project)
+ end
+
+ let!(:second_commit_diff_note) do
+ create(:diff_note_on_commit, project: merge_request.project)
+ end
+
+ it 'preloads all of the notes diffs highlights' do
+ expect_next_instance_of(Gitlab::DiscussionsDiff::FileCollection) do |collection|
+ first_note_diff_file = first_commit_diff_note.note_diff_file
+ second_note_diff_file = second_commit_diff_note.note_diff_file
+
+ expect(collection).to receive(:load_highlight).and_call_original
+ expect(collection).to receive(:find_by_id).with(first_note_diff_file.id).and_call_original
+ expect(collection).to receive(:find_by_id).with(second_note_diff_file.id).and_call_original
+ end
- expect(collection).to receive(:load_highlight).and_call_original
- expect(collection).to receive(:find_by_id).with(note_diff_file.id).and_call_original
+ get :discussions, params: { namespace_id: project.namespace, project_id: project, id: merge_request.iid,
+ per_page: 2 }
end
- get :discussions, params: { namespace_id: project.namespace, project_id: project, id: merge_request.iid }
+ it 'preloads all of the notes diffs highlights when per_page is 1' do
+ expect_next_instance_of(Gitlab::DiscussionsDiff::FileCollection) do |collection|
+ first_note_diff_file = first_commit_diff_note.note_diff_file
+ second_note_diff_file = second_commit_diff_note.note_diff_file
+
+ expect(collection).to receive(:load_highlight).and_call_original
+ expect(collection).to receive(:find_by_id).with(first_note_diff_file.id).and_call_original
+ expect(collection).not_to receive(:find_by_id).with(second_note_diff_file.id)
+ end
+
+ get :discussions, params: { namespace_id: project.namespace, project_id: project, id: merge_request.iid,
+ per_page: 1 }
+ end
end
- end
- context 'with diff notes' do
- let!(:diff_note) do
- create(:diff_note_on_merge_request, noteable: merge_request, project: merge_request.project)
+ context 'with diff notes' do
+ let!(:diff_note) do
+ create(:diff_note_on_merge_request, noteable: merge_request, project: merge_request.project)
+ end
+
+ it 'preloads notes diffs highlights' do
+ expect_next_instance_of(Gitlab::DiscussionsDiff::FileCollection) do |collection|
+ note_diff_file = diff_note.note_diff_file
+
+ expect(collection).to receive(:load_highlight).and_call_original
+ expect(collection).to receive(:find_by_id).with(note_diff_file.id).and_call_original
+ end
+
+ get :discussions, params: { namespace_id: project.namespace, project_id: project, id: merge_request.iid }
+ end
end
+ end
- it 'preloads notes diffs highlights' do
- expect_next_instance_of(Gitlab::DiscussionsDiff::FileCollection) do |collection|
- note_diff_file = diff_note.note_diff_file
+ context 'when only_highlight_discussions_requested is true' do
+ context 'with commit diff notes' do
+ let!(:first_commit_diff_note) do
+ create(:diff_note_on_commit, project: merge_request.project)
+ end
- expect(collection).to receive(:load_highlight).and_call_original
- expect(collection).to receive(:find_by_id).with(note_diff_file.id).and_call_original
+ let!(:second_commit_diff_note) do
+ create(:diff_note_on_commit, project: merge_request.project)
end
- get :discussions, params: { namespace_id: project.namespace, project_id: project, id: merge_request.iid }
- end
+ it 'preloads all of the notes diffs highlights' do
+ expect_next_instance_of(Gitlab::DiscussionsDiff::FileCollection) do |collection|
+ first_note_diff_file = first_commit_diff_note.note_diff_file
+ second_note_diff_file = second_commit_diff_note.note_diff_file
+
+ expect(collection).to receive(:load_highlight).with(diff_note_ids: [first_commit_diff_note.id, second_commit_diff_note.id]).and_call_original
+ expect(collection).to receive(:find_by_id).with(first_note_diff_file.id).and_call_original
+ expect(collection).to receive(:find_by_id).with(second_note_diff_file.id).and_call_original
+ end
+
+ get :discussions, params: { namespace_id: project.namespace, project_id: project, id: merge_request.iid,
+ per_page: 2 }
+ end
+
+ it 'preloads all of the notes diffs highlights when per_page is 1' do
+ expect_next_instance_of(Gitlab::DiscussionsDiff::FileCollection) do |collection|
+ first_note_diff_file = first_commit_diff_note.note_diff_file
+ second_note_diff_file = second_commit_diff_note.note_diff_file
- it 'does not preload highlights when diff note is resolved' do
- Notes::ResolveService.new(diff_note.project, user).execute(diff_note)
+ expect(collection).to receive(:load_highlight).with(diff_note_ids: [first_commit_diff_note.id]).and_call_original
+ expect(collection).to receive(:find_by_id).with(first_note_diff_file.id).and_call_original
+ expect(collection).not_to receive(:find_by_id).with(second_note_diff_file.id)
+ end
- expect_next_instance_of(Gitlab::DiscussionsDiff::FileCollection) do |collection|
- note_diff_file = diff_note.note_diff_file
+ get :discussions, params: { namespace_id: project.namespace, project_id: project, id: merge_request.iid,
+ per_page: 1 }
+ end
+ end
- expect(collection).to receive(:load_highlight).and_call_original
- expect(collection).to receive(:find_by_id).with(note_diff_file.id).and_call_original
+ context 'with diff notes' do
+ let!(:diff_note) do
+ create(:diff_note_on_merge_request, noteable: merge_request, project: merge_request.project)
end
- get :discussions, params: { namespace_id: project.namespace, project_id: project, id: merge_request.iid }
+ it 'preloads notes diffs highlights' do
+ expect_next_instance_of(Gitlab::DiscussionsDiff::FileCollection) do |collection|
+ note_diff_file = diff_note.note_diff_file
+
+ expect(collection).to receive(:load_highlight).with(diff_note_ids: [diff_note.id]).and_call_original
+ expect(collection).to receive(:find_by_id).with(note_diff_file.id).and_call_original
+ end
+
+ get :discussions, params: { namespace_id: project.namespace, project_id: project, id: merge_request.iid }
+ end
end
end
end
- end
- context do
- it_behaves_like 'discussions provider' do
- let!(:author) { create(:user) }
- let!(:project) { create(:project) }
+ context do
+ it_behaves_like 'discussions provider' do
+ let!(:author) { create(:user) }
+ let_it_be_with_refind(:project) { create(:project) }
- let!(:merge_request) { create(:merge_request, source_project: project) }
+ let!(:merge_request) { create(:merge_request, source_project: project) }
- let!(:mr_note1) { create(:discussion_note_on_merge_request, noteable: merge_request, project: project) }
- let!(:mr_note2) { create(:discussion_note_on_merge_request, noteable: merge_request, project: project) }
+ let!(:mr_note1) { create(:discussion_note_on_merge_request, noteable: merge_request, project: project) }
+ let!(:mr_note2) { create(:discussion_note_on_merge_request, noteable: merge_request, project: project) }
- let(:requested_iid) { merge_request.iid }
- let(:expected_discussion_count) { 2 }
- let(:expected_discussion_ids) { [mr_note1.discussion_id, mr_note2.discussion_id] }
+ let(:requested_iid) { merge_request.iid }
+ let(:expected_discussion_count) { 2 }
+ let(:expected_discussion_ids) { [mr_note1.discussion_id, mr_note2.discussion_id] }
+ end
end
end
end
diff --git a/spec/controllers/projects/pages_controller_spec.rb b/spec/controllers/projects/pages_controller_spec.rb
index 34ec8d8d575..d94150a37d0 100644
--- a/spec/controllers/projects/pages_controller_spec.rb
+++ b/spec/controllers/projects/pages_controller_spec.rb
@@ -48,7 +48,6 @@ RSpec.describe Projects::PagesController, feature_category: :pages do
context 'when the project does not have onboarding complete' do
before do
- project.pages_metadatum.update_attribute(:deployed, false)
project.pages_metadatum.update_attribute(:onboarding_complete, false)
end
@@ -76,6 +75,17 @@ RSpec.describe Projects::PagesController, feature_category: :pages do
end
end
+ context 'when the project has a deployed pages app' do
+ before do
+ project.pages_metadatum.update_attribute(:onboarding_complete, false)
+ create(:pages_deployment, project: project)
+ end
+
+ it 'does not redirect to #new' do
+ expect(subject).not_to redirect_to(action: 'new')
+ end
+ end
+
context 'when pages is disabled' do
let(:project) { create(:project, :pages_disabled) }
diff --git a/spec/controllers/projects/tree_controller_spec.rb b/spec/controllers/projects/tree_controller_spec.rb
index a409030e359..4e00d58bf17 100644
--- a/spec/controllers/projects/tree_controller_spec.rb
+++ b/spec/controllers/projects/tree_controller_spec.rb
@@ -5,7 +5,6 @@ require 'spec_helper'
RSpec.describe Projects::TreeController, feature_category: :source_code_management do
let_it_be(:project) { create(:project, :repository) }
let(:user) { create(:user) }
- let(:redirect_with_ref_type) { true }
before do
sign_in(user)
@@ -25,20 +24,31 @@ RSpec.describe Projects::TreeController, feature_category: :source_code_manageme
let(:ref_type) { nil }
+ let(:redirect_with_ref_type) { true }
+
# Make sure any errors accessing the tree in our views bubble up to this spec
render_views
- before do
- expect(::Gitlab::GitalyClient).to receive(:allow_ref_name_caching).and_call_original
- project.repository.add_tag(project.creator, 'ambiguous_ref', RepoHelpers.sample_commit.id)
- project.repository.add_branch(project.creator, 'ambiguous_ref', RepoHelpers.another_sample_commit.id)
+ include_context 'with ambiguous refs for controllers'
- stub_feature_flags(redirect_with_ref_type: redirect_with_ref_type)
- end
+ describe '#set_is_ambiguous_ref before action' do
+ let(:redirect_with_ref_type) { false }
+
+ before do
+ request
+ end
- after do
- project.repository.rm_tag(project.creator, 'ambiguous_ref')
- project.repository.rm_branch(project.creator, 'ambiguous_ref')
+ context 'when ref requested is ambiguous with no ref type' do
+ let(:id) { 'ambiguous_ref' }
+
+ it_behaves_like '#set_is_ambiguous_ref when ref is ambiguous'
+ end
+
+ context 'when ref requested is not ambiguous' do
+ let(:id) { 'master' }
+
+ it_behaves_like '#set_is_ambiguous_ref when ref is not ambiguous'
+ end
end
context 'when the redirect_with_ref_type flag is disabled' do
diff --git a/spec/controllers/repositories/git_http_controller_spec.rb b/spec/controllers/repositories/git_http_controller_spec.rb
index 602c9c0a2ce..0ae44d3654e 100644
--- a/spec/controllers/repositories/git_http_controller_spec.rb
+++ b/spec/controllers/repositories/git_http_controller_spec.rb
@@ -200,4 +200,24 @@ RSpec.describe Repositories::GitHttpController, feature_category: :source_code_m
end
end
end
+
+ describe '#append_info_to_payload' do
+ let(:log_payload) { {} }
+ let(:container) { project.design_management_repository }
+ let(:repository_path) { "#{container.full_path}.git" }
+ let(:params) { { repository_path: repository_path, service: 'git-upload-pack' } }
+ let(:repository_storage) { "default" }
+
+ before do
+ allow(controller).to receive(:append_info_to_payload).and_wrap_original do |method, *|
+ method.call(log_payload)
+ end
+ end
+
+ it 'appends metadata for logging' do
+ post :git_upload_pack, params: params
+ expect(controller).to have_received(:append_info_to_payload)
+ expect(log_payload.dig(:metadata, :repository_storage)).to eq(repository_storage)
+ end
+ end
end
diff --git a/spec/controllers/search_controller_spec.rb b/spec/controllers/search_controller_spec.rb
index 94aedf463e9..9453520341b 100644
--- a/spec/controllers/search_controller_spec.rb
+++ b/spec/controllers/search_controller_spec.rb
@@ -537,6 +537,34 @@ RSpec.describe SearchController, feature_category: :global_search do
expect(response.headers['Cache-Control']).to eq('max-age=60, private')
expect(response.headers['Pragma']).to be_nil
end
+
+ context 'unique users tracking' do
+ before do
+ allow(Gitlab::UsageDataCounters::HLLRedisCounter).to receive(:track_event)
+ end
+
+ it_behaves_like 'tracking unique hll events' do
+ subject(:request) { get :autocomplete, params: { term: 'term' } }
+
+ let(:target_event) { 'i_search_total' }
+ let(:expected_value) { instance_of(String) }
+ end
+ end
+
+ it_behaves_like 'Snowplow event tracking with RedisHLL context' do
+ subject { get :autocomplete, params: { group_id: namespace.id, term: 'term' } }
+
+ let(:project) { nil }
+ let(:category) { described_class.to_s }
+ let(:action) { 'autocomplete' }
+ let(:label) { 'redis_hll_counters.search.search_total_unique_counts_monthly' }
+ let(:property) { 'i_search_total' }
+ let(:context) do
+ [Gitlab::Tracking::ServicePingContext.new(data_source: :redis_hll, event: property).to_context]
+ end
+
+ let(:namespace) { create(:group) }
+ end
end
describe '#append_info_to_payload' do
diff --git a/spec/db/docs_spec.rb b/spec/db/docs_spec.rb
index 8d4cb3ac5ef..19edf3da0d5 100644
--- a/spec/db/docs_spec.rb
+++ b/spec/db/docs_spec.rb
@@ -14,6 +14,7 @@ RSpec.shared_examples 'validate dictionary' do |objects, directory_path, require
introduced_by_url
milestone
gitlab_schema
+ schema_inconsistencies
]
end
@@ -169,7 +170,8 @@ RSpec.shared_examples 'validate dictionary' do |objects, directory_path, require
end
RSpec.describe 'Views documentation', feature_category: :database do
- database_base_models = Gitlab::Database.database_base_models.select { |k, _| k != 'geo' }
+ excluded = %w[geo jh]
+ database_base_models = Gitlab::Database.database_base_models.reject { |k, _| k.in?(excluded) }
views = database_base_models.flat_map { |_, m| m.connection.views }.sort.uniq
directory_path = File.join('db', 'docs', 'views')
required_fields = %i[feature_categories view_name gitlab_schema]
@@ -178,7 +180,8 @@ RSpec.describe 'Views documentation', feature_category: :database do
end
RSpec.describe 'Tables documentation', feature_category: :database do
- database_base_models = Gitlab::Database.database_base_models.select { |k, _| k != 'geo' }
+ excluded = %w[geo jh]
+ database_base_models = Gitlab::Database.database_base_models.reject { |k, _| k.in?(excluded) }
tables = database_base_models.flat_map { |_, m| m.connection.tables }.sort.uniq
directory_path = File.join('db', 'docs')
required_fields = %i[feature_categories table_name gitlab_schema]
diff --git a/spec/db/migration_spec.rb b/spec/db/migration_spec.rb
index b7a4a302290..784f8753893 100644
--- a/spec/db/migration_spec.rb
+++ b/spec/db/migration_spec.rb
@@ -8,7 +8,8 @@ RSpec.describe 'Migrations Validation', feature_category: :database do
# The range describes the timestamps that given migration helper can be used
let(:all_migration_classes) do
{
- 2022_12_01_02_15_00.. => Gitlab::Database::Migration[2.1],
+ 2023_10_10_02_15_00.. => Gitlab::Database::Migration[2.2],
+ 2022_12_01_02_15_00..2023_11_01_02_15_00 => Gitlab::Database::Migration[2.1],
2022_01_26_21_06_58..2023_01_11_12_45_12 => Gitlab::Database::Migration[2.0],
2021_09_01_15_33_24..2022_04_25_12_06_03 => Gitlab::Database::Migration[1.0],
2021_05_31_05_39_16..2021_09_01_15_33_24 => ActiveRecord::Migration[6.1],
diff --git a/spec/experiments/ios_specific_templates_experiment_spec.rb b/spec/experiments/ios_specific_templates_experiment_spec.rb
deleted file mode 100644
index 909ac22b97b..00000000000
--- a/spec/experiments/ios_specific_templates_experiment_spec.rb
+++ /dev/null
@@ -1,62 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe IosSpecificTemplatesExperiment do
- subject do
- described_class.new(actor: user, project: project) do |e|
- e.candidate { true }
- end.run
- end
-
- let_it_be(:user) { create(:user) }
- let_it_be(:project) { create(:project, :auto_devops_disabled) }
-
- let!(:project_setting) { create(:project_setting, project: project, target_platforms: target_platforms) }
- let(:target_platforms) { %w[ios] }
-
- before do
- stub_experiments(ios_specific_templates: :candidate)
- project.add_developer(user) if user
- end
-
- it { is_expected.to be true }
-
- describe 'skipping the experiment' do
- context 'no actor' do
- let_it_be(:user) { nil }
-
- it { is_expected.to be_falsey }
- end
-
- context 'actor cannot create pipelines' do
- before do
- project.add_guest(user)
- end
-
- it { is_expected.to be_falsey }
- end
-
- context 'targeting a non iOS platform' do
- let(:target_platforms) { [] }
-
- it { is_expected.to be_falsey }
- end
-
- context 'project has a ci.yaml file' do
- before do
- allow(project).to receive(:has_ci?).and_return(true)
- end
-
- it { is_expected.to be_falsey }
- end
-
- context 'project has pipelines' do
- before do
- create(:ci_pipeline, project: project)
- end
-
- it { is_expected.to be_falsey }
- end
- end
-end
diff --git a/spec/factories/activity_pub/releases_subscriptions.rb b/spec/factories/activity_pub/releases_subscriptions.rb
new file mode 100644
index 00000000000..b789188528a
--- /dev/null
+++ b/spec/factories/activity_pub/releases_subscriptions.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+FactoryBot.define do
+ factory :activity_pub_releases_subscription, class: 'ActivityPub::ReleasesSubscription' do
+ project
+ subscriber_url { 'https://example.com/actor' }
+ status { :requested }
+ payload do
+ {
+ '@context': 'https://www.w3.org/ns/activitystreams',
+ id: 'https://example.com/actor#follow/1',
+ type: 'Follow',
+ actor: 'https://example.com/actor',
+ object: 'http://localhost/user/project/-/releases'
+ }
+ end
+
+ trait :inbox do
+ subscriber_inbox_url { 'https://example.com/actor/inbox' }
+ end
+
+ trait :shared_inbox do
+ shared_inbox_url { 'https://example.com/shared-inbox' }
+ end
+ end
+end
diff --git a/spec/factories/ai/service_access_tokens.rb b/spec/factories/ai/service_access_tokens.rb
index 61abf4e1144..0598eed52c4 100644
--- a/spec/factories/ai/service_access_tokens.rb
+++ b/spec/factories/ai/service_access_tokens.rb
@@ -4,7 +4,6 @@ FactoryBot.define do
factory :service_access_token, class: 'Ai::ServiceAccessToken' do
token { SecureRandom.alphanumeric(10) }
expires_at { Time.current + 1.day }
- category { :code_suggestions }
trait :active do
expires_at { Time.current + 1.day }
@@ -13,9 +12,5 @@ FactoryBot.define do
trait :expired do
expires_at { Time.current - 1.day }
end
-
- trait :code_suggestions do
- category { :code_suggestions }
- end
end
end
diff --git a/spec/factories/ci/build_trace_chunks.rb b/spec/factories/ci/build_trace_chunks.rb
index 64a297932de..56d8fc23222 100644
--- a/spec/factories/ci/build_trace_chunks.rb
+++ b/spec/factories/ci/build_trace_chunks.rb
@@ -22,6 +22,22 @@ FactoryBot.define do
data_store { :redis }
end
+ trait :redis_trace_chunks_with_data do
+ data_store { :redis_trace_chunks }
+
+ transient do
+ initial_data { 'test data' }
+ end
+
+ after(:create) do |build_trace_chunk, evaluator|
+ Ci::BuildTraceChunks::RedisTraceChunks.new.set_data(build_trace_chunk, evaluator.initial_data)
+ end
+ end
+
+ trait :redis_trace_chunks_without_data do
+ data_store { :redis_trace_chunks }
+ end
+
trait :database_with_data do
data_store { :database }
diff --git a/spec/factories/ci/builds.rb b/spec/factories/ci/builds.rb
index 867db96aaaf..18415a6079f 100644
--- a/spec/factories/ci/builds.rb
+++ b/spec/factories/ci/builds.rb
@@ -117,6 +117,11 @@ FactoryBot.define do
status { 'running' }
end
+ trait :waiting_for_callback do
+ started
+ status { 'waiting_for_callback' }
+ end
+
trait :pending do
with_token
queued_at { 'Di 29. Okt 09:50:59 CET 2013' }
diff --git a/spec/factories/ci/catalog/resources/components.rb b/spec/factories/ci/catalog/resources/components.rb
index 8feecc695bc..843ccb2b461 100644
--- a/spec/factories/ci/catalog/resources/components.rb
+++ b/spec/factories/ci/catalog/resources/components.rb
@@ -5,6 +5,6 @@ FactoryBot.define do
version factory: :ci_catalog_resource_version
catalog_resource { version.catalog_resource }
project { version.project }
- name { catalog_resource.name }
+ name { catalog_resource.project.name }
end
end
diff --git a/spec/factories/ci/pipeline_artifacts.rb b/spec/factories/ci/pipeline_artifacts.rb
index bdd390126dd..77b1ac5a9cc 100644
--- a/spec/factories/ci/pipeline_artifacts.rb
+++ b/spec/factories/ci/pipeline_artifacts.rb
@@ -22,14 +22,6 @@ FactoryBot.define do
locked { :unlocked }
end
- trait :checksummed do
- verification_checksum { 'abc' }
- end
-
- trait :checksum_failure do
- verification_failure { 'Could not calculate the checksum' }
- end
-
trait :expired do
expire_at { Date.yesterday }
end
diff --git a/spec/factories/ci/reports/security/findings.rb b/spec/factories/ci/reports/security/findings.rb
index 202c2789b45..670d833c1f8 100644
--- a/spec/factories/ci/reports/security/findings.rb
+++ b/spec/factories/ci/reports/security/findings.rb
@@ -10,7 +10,7 @@ FactoryBot.define do
metadata_version { 'sast:1.0' }
name { 'Cipher with no integrity' }
report_type { :sast }
- cvss { [{ vendor: "GitLab", vector_string: "CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:U/C:L/I:L/A:N" }] }
+ cvss { [{ vendor: "GitLab", vector: "CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:U/C:L/I:L/A:N" }] }
original_data do
{
description: "The cipher does not provide data integrity update 1",
diff --git a/spec/factories/commit_statuses.rb b/spec/factories/commit_statuses.rb
index 7d0176d0683..e0b34bc39d8 100644
--- a/spec/factories/commit_statuses.rb
+++ b/spec/factories/commit_statuses.rb
@@ -32,6 +32,10 @@ FactoryBot.define do
status { 'running' }
end
+ trait :waiting_for_callback do
+ status { 'waiting_for_callback' }
+ end
+
trait :pending do
status { 'pending' }
end
diff --git a/spec/factories/ml/model_metadata.rb b/spec/factories/ml/model_metadata.rb
new file mode 100644
index 00000000000..03ceaffa6bc
--- /dev/null
+++ b/spec/factories/ml/model_metadata.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+FactoryBot.define do
+ factory :ml_model_metadata, class: '::Ml::ModelMetadata' do
+ association :model, factory: :ml_models
+
+ sequence(:name) { |n| "metadata_#{n}" }
+ sequence(:value) { |n| "value#{n}" }
+ end
+end
diff --git a/spec/factories/ml/models.rb b/spec/factories/ml/models.rb
index 158c26499b0..3377a54f265 100644
--- a/spec/factories/ml/models.rb
+++ b/spec/factories/ml/models.rb
@@ -18,5 +18,11 @@ FactoryBot.define do
versions { [version] }
latest_version { version }
end
+
+ trait :with_metadata do
+ after(:create) do |model|
+ model.metadata = FactoryBot.create_list(:ml_model_metadata, 2, model: model) # rubocop:disable StrategyInCallback
+ end
+ end
end
end
diff --git a/spec/factories/packages/npm/metadata_cache.rb b/spec/factories/packages/npm/metadata_cache.rb
index e76ddf3c983..4fe1930d03e 100644
--- a/spec/factories/packages/npm/metadata_cache.rb
+++ b/spec/factories/packages/npm/metadata_cache.rb
@@ -6,5 +6,15 @@ FactoryBot.define do
sequence(:package_name) { |n| "@#{project.root_namespace.path}/package-#{n}" }
file { fixture_file_upload('spec/fixtures/packages/npm/metadata.json') }
size { 401.bytes }
+
+ trait :processing do
+ status { 'processing' }
+ end
+
+ trait :stale do
+ after(:create) do |entry|
+ entry.update_attribute(:project_id, nil)
+ end
+ end
end
end
diff --git a/spec/factories/packages/nuget/symbol.rb b/spec/factories/packages/nuget/symbol.rb
index 7ab1e026cda..665535de939 100644
--- a/spec/factories/packages/nuget/symbol.rb
+++ b/spec/factories/packages/nuget/symbol.rb
@@ -7,5 +7,6 @@ FactoryBot.define do
file_path { 'lib/net7.0/package.pdb' }
size { 100.bytes }
sequence(:signature) { |n| "b91a152048fc4b3883bf3cf73fbc03f#{n}FFFFFFFF" }
+ file_sha256 { 'dd1aaf26c557685cc37f93f53a2b6befb2c2e679f5ace6ec7a26d12086f358be' }
end
end
diff --git a/spec/factories/pages_domains.rb b/spec/factories/pages_domains.rb
index d91037e803f..5b78c83251a 100644
--- a/spec/factories/pages_domains.rb
+++ b/spec/factories/pages_domains.rb
@@ -150,6 +150,87 @@ NVOFBkpdn627G190
end
end
+ trait :with_untrusted_root_ca_in_chain do
+ # This contains
+ # [Intermediate #2 (SHA-2)] 'CloudFlare Origin SSL Certificate Authority'
+ # [Intermediate #1 (SHA-2)] 'CloudFlare Origin Certificate'
+ certificate do
+ '-----BEGIN CERTIFICATE-----
+MIIGCDCCA/CgAwIBAgIQKy5u6tl1NmwUim7bo3yMBzANBgkqhkiG9w0BAQwFADCB
+hTELMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4G
+A1UEBxMHU2FsZm9yZDEaMBgGA1UEChMRQ09NT0RPIENBIExpbWl0ZWQxKzApBgNV
+BAMTIkNPTU9ETyBSU0EgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMTQwMjEy
+MDAwMDAwWhcNMjkwMjExMjM1OTU5WjCBkDELMAkGA1UEBhMCR0IxGzAZBgNVBAgT
+EkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEaMBgGA1UEChMR
+Q09NT0RPIENBIExpbWl0ZWQxNjA0BgNVBAMTLUNPTU9ETyBSU0EgRG9tYWluIFZh
+bGlkYXRpb24gU2VjdXJlIFNlcnZlciBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEP
+ADCCAQoCggEBAI7CAhnhoFmk6zg1jSz9AdDTScBkxwtiBUUWOqigwAwCfx3M28Sh
+bXcDow+G+eMGnD4LgYqbSRutA776S9uMIO3Vzl5ljj4Nr0zCsLdFXlIvNN5IJGS0
+Qa4Al/e+Z96e0HqnU4A7fK31llVvl0cKfIWLIpeNs4TgllfQcBhglo/uLQeTnaG6
+ytHNe+nEKpooIZFNb5JPJaXyejXdJtxGpdCsWTWM/06RQ1A/WZMebFEh7lgUq/51
+UHg+TLAchhP6a5i84DuUHoVS3AOTJBhuyydRReZw3iVDpA3hSqXttn7IzW3uLh0n
+c13cRTCAquOyQQuvvUSH2rnlG51/ruWFgqUCAwEAAaOCAWUwggFhMB8GA1UdIwQY
+MBaAFLuvfgI9+qbxPISOre44mOzZMjLUMB0GA1UdDgQWBBSQr2o6lFoL2JDqElZz
+30O0Oija5zAOBgNVHQ8BAf8EBAMCAYYwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNV
+HSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwGwYDVR0gBBQwEjAGBgRVHSAAMAgG
+BmeBDAECATBMBgNVHR8ERTBDMEGgP6A9hjtodHRwOi8vY3JsLmNvbW9kb2NhLmNv
+bS9DT01PRE9SU0FDZXJ0aWZpY2F0aW9uQXV0aG9yaXR5LmNybDBxBggrBgEFBQcB
+AQRlMGMwOwYIKwYBBQUHMAKGL2h0dHA6Ly9jcnQuY29tb2RvY2EuY29tL0NPTU9E
+T1JTQUFkZFRydXN0Q0EuY3J0MCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5jb21v
+ZG9jYS5jb20wDQYJKoZIhvcNAQEMBQADggIBAE4rdk+SHGI2ibp3wScF9BzWRJ2p
+mj6q1WZmAT7qSeaiNbz69t2Vjpk1mA42GHWx3d1Qcnyu3HeIzg/3kCDKo2cuH1Z/
+e+FE6kKVxF0NAVBGFfKBiVlsit2M8RKhjTpCipj4SzR7JzsItG8kO3KdY3RYPBps
+P0/HEZrIqPW1N+8QRcZs2eBelSaz662jue5/DJpmNXMyYE7l3YphLG5SEXdoltMY
+dVEVABt0iN3hxzgEQyjpFv3ZBdRdRydg1vs4O2xyopT4Qhrf7W8GjEXCBgCq5Ojc
+2bXhc3js9iPc0d1sjhqPpepUfJa3w/5Vjo1JXvxku88+vZbrac2/4EjxYoIQ5QxG
+V/Iz2tDIY+3GH5QFlkoakdH368+PUq4NCNk+qKBR6cGHdNXJ93SrLlP7u3r7l+L4
+HyaPs9Kg4DdbKDsx5Q5XLVq4rXmsXiBmGqW5prU5wfWYQ//u+aen/e7KJD2AFsQX
+j4rBYKEMrltDR5FL1ZoXX/nUh8HCjLfn4g8wGTeGrODcQgPmlKidrv0PJFGUzpII
+0fxQ8ANAe4hZ7Q7drNJ3gjTcBpUC2JD5Leo31Rpg0Gcg19hCC0Wvgmje3WYkN5Ap
+lBlGGSW4gNfL1IYoakRwJiNiqZ+Gb7+6kHDSVneFeO/qJakXzlByjAA6quPbYzSf
++AZxAeKCINT+b72x
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIF2DCCA8CgAwIBAgIQTKr5yttjb+Af907YWwOGnTANBgkqhkiG9w0BAQwFADCB
+hTELMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4G
+A1UEBxMHU2FsZm9yZDEaMBgGA1UEChMRQ09NT0RPIENBIExpbWl0ZWQxKzApBgNV
+BAMTIkNPTU9ETyBSU0EgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMTAwMTE5
+MDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCBhTELMAkGA1UEBhMCR0IxGzAZBgNVBAgT
+EkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEaMBgGA1UEChMR
+Q09NT0RPIENBIExpbWl0ZWQxKzApBgNVBAMTIkNPTU9ETyBSU0EgQ2VydGlmaWNh
+dGlvbiBBdXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCR
+6FSS0gpWsawNJN3Fz0RndJkrN6N9I3AAcbxT38T6KhKPS38QVr2fcHK3YX/JSw8X
+pz3jsARh7v8Rl8f0hj4K+j5c+ZPmNHrZFGvnnLOFoIJ6dq9xkNfs/Q36nGz637CC
+9BR++b7Epi9Pf5l/tfxnQ3K9DADWietrLNPtj5gcFKt+5eNu/Nio5JIk2kNrYrhV
+/erBvGy2i/MOjZrkm2xpmfh4SDBF1a3hDTxFYPwyllEnvGfDyi62a+pGx8cgoLEf
+Zd5ICLqkTqnyg0Y3hOvozIFIQ2dOciqbXL1MGyiKXCJ7tKuY2e7gUYPDCUZObT6Z
++pUX2nwzV0E8jVHtC7ZcryxjGt9XyD+86V3Em69FmeKjWiS0uqlWPc9vqv9JWL7w
+qP/0uK3pN/u6uPQLOvnoQ0IeidiEyxPx2bvhiWC4jChWrBQdnArncevPDt09qZah
+SL0896+1DSJMwBGB7FY79tOi4lu3sgQiUpWAk2nojkxl8ZEDLXB0AuqLZxUpaVIC
+u9ffUGpVRr+goyhhf3DQw6KqLCGqR84onAZFdr+CGCe01a60y1Dma/RMhnEw6abf
+Fobg2P9A3fvQQoh/ozM6LlweQRGBY84YcWsr7KaKtzFcOmpH4MN5WdYgGq/yapiq
+crxXStJLnbsQ/LBMQeXtHT1eKJ2czL+zUdqnR+WEUwIDAQABo0IwQDAdBgNVHQ4E
+FgQUu69+Aj36pvE8hI6t7jiY7NkyMtQwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB
+/wQFMAMBAf8wDQYJKoZIhvcNAQEMBQADggIBAArx1UaEt65Ru2yyTUEUAJNMnMvl
+wFTPoCWOAvn9sKIN9SCYPBMtrFaisNZ+EZLpLrqeLppysb0ZRGxhNaKatBYSaVqM
+4dc+pBroLwP0rmEdEBsqpIt6xf4FpuHA1sj+nq6PK7o9mfjYcwlYRm6mnPTXJ9OV
+2jeDchzTc+CiR5kDOF3VSXkAKRzH7JsgHAckaVd4sjn8OoSgtZx8jb8uk2Intzna
+FxiuvTwJaP+EmzzV1gsD41eeFPfR60/IvYcjt7ZJQ3mFXLrrkguhxuhoqEwWsRqZ
+CuhTLJK7oQkYdQxlqHvLI7cawiiFwxv/0Cti76R7CZGYZ4wUAc1oBmpjIXUDgIiK
+boHGhfKppC3n9KUkEEeDys30jXlYsQab5xoq2Z0B15R97QNKyvDb6KkBPvVWmcke
+jkk9u+UJueBPSZI9FoJAzMxZxuY67RIuaTxslbH9qh17f4a+Hg4yRvv7E491f0yL
+S0Zj/gA0QHDBw7mh3aZw4gSzQbzpgJHqZJx64SIDqZxubw5lT2yHh17zbqD5daWb
+QOhTsiedSrnAdyGN/4fy3ryM7xfft0kL0fJuMAsaDk527RH89elWsn2/x20Kk4yl
+0MC2Hb46TpSi125sC8KKfPog88Tk5c0NqMuRkrF8hey1FGlmDoLnzc7ILaZRfyHB
+NVOFBkpdn627G190
+-----END CERTIFICATE-----'
+ end
+
+ key do
+ File.read(Rails.root.join('spec/fixtures/', 'origin_cert_key.pem'))
+ end
+ end
+
trait :with_trusted_expired_chain do
# This contains
# Let's Encrypt Authority X3
diff --git a/spec/factories/projects.rb b/spec/factories/projects.rb
index 443bca6030c..1e3ade779af 100644
--- a/spec/factories/projects.rb
+++ b/spec/factories/projects.rb
@@ -94,6 +94,8 @@ FactoryBot.define do
visibility_level: evaluator.visibility_level
}
+ project_namespace_hash[:id] = evaluator.project_namespace_id.presence
+
project.build_project_namespace(project_namespace_hash)
project.build_project_feature(project_feature_hash)
@@ -256,6 +258,35 @@ FactoryBot.define do
end
end
+ # A catalog resource repository with a file structure set up for ci components.
+ trait :catalog_resource_with_components do
+ small_repo
+ description { 'catalog resource' }
+
+ files do
+ {
+ 'templates/secret-detection.yml' => "spec:\n inputs:\n website:\n---\nimage: alpine_1",
+ 'templates/dast/template.yml' => 'image: alpine_2',
+ 'templates/template.yml' => 'image: alpine_3',
+ 'templates/blank-yaml.yml' => '',
+ 'README.md' => 'readme'
+ }
+ end
+
+ transient do
+ create_tag { nil }
+ end
+
+ after(:create) do |project, evaluator|
+ if evaluator.create_tag
+ project.repository.add_tag(
+ project.creator,
+ evaluator.create_tag,
+ project.repository.commit.sha)
+ end
+ end
+ end
+
# A basic repository with a single file 'test.txt'. It also has the HEAD as the default branch.
trait :small_repo do
custom_repo
@@ -477,7 +508,7 @@ FactoryBot.define do
trait :pages_published do
after(:create) do |project|
project.mark_pages_onboarding_complete
- project.mark_pages_as_deployed
+ create(:pages_deployment, project: project) # rubocop: disable RSpec/FactoryBot/StrategyInCallback
end
end
diff --git a/spec/factories/snippet_repositories.rb b/spec/factories/snippet_repositories.rb
index c3a6bc3ae31..1f9e68514bb 100644
--- a/spec/factories/snippet_repositories.rb
+++ b/spec/factories/snippet_repositories.rb
@@ -8,13 +8,5 @@ FactoryBot.define do
snippet_repository.shard_name = snippet_repository.snippet.repository_storage
snippet_repository.disk_path = snippet_repository.snippet.disk_path
end
-
- trait(:checksummed) do
- verification_checksum { 'abc' }
- end
-
- trait(:checksum_failure) do
- verification_failure { 'Could not calculate the checksum' }
- end
end
end
diff --git a/spec/factories/terraform/state_version.rb b/spec/factories/terraform/state_version.rb
index c6bd08815cf..5386dfa98f2 100644
--- a/spec/factories/terraform/state_version.rb
+++ b/spec/factories/terraform/state_version.rb
@@ -8,13 +8,5 @@ FactoryBot.define do
sequence(:version)
file { fixture_file_upload('spec/fixtures/terraform/terraform.tfstate', 'application/json') }
-
- trait(:checksummed) do
- verification_checksum { 'abc' }
- end
-
- trait(:checksum_failure) do
- verification_failure { 'Could not calculate the checksum' }
- end
end
end
diff --git a/spec/factories/users.rb b/spec/factories/users.rb
index de2b5159fe7..8b42631040e 100644
--- a/spec/factories/users.rb
+++ b/spec/factories/users.rb
@@ -135,10 +135,6 @@ FactoryBot.define do
end
end
- trait :no_super_sidebar do
- use_new_navigation { false }
- end
-
trait :two_factor_via_webauthn do
transient { registrations_count { 5 } }
diff --git a/spec/factories/users/phone_number_validations.rb b/spec/factories/users/phone_number_validations.rb
index da53dda89b4..b7e6e819127 100644
--- a/spec/factories/users/phone_number_validations.rb
+++ b/spec/factories/users/phone_number_validations.rb
@@ -6,5 +6,6 @@ FactoryBot.define do
country { 'US' }
international_dial_code { 1 }
phone_number { '555' }
+ telesign_reference_xid { FFaker::Guid.guid }
end
end
diff --git a/spec/fast_spec_helper.rb b/spec/fast_spec_helper.rb
index 7ffab4554cf..d03f8b18b3e 100644
--- a/spec/fast_spec_helper.rb
+++ b/spec/fast_spec_helper.rb
@@ -8,6 +8,7 @@ end
require_relative '../config/bundler_setup'
+ENV['GITLAB_ENV'] = 'test'
ENV['IN_MEMORY_APPLICATION_SETTINGS'] = 'true'
require './spec/deprecation_warnings'
diff --git a/spec/features/abuse_report_spec.rb b/spec/features/abuse_report_spec.rb
index f1df5c2d6f0..eac29b0b741 100644
--- a/spec/features/abuse_report_spec.rb
+++ b/spec/features/abuse_report_spec.rb
@@ -3,9 +3,9 @@
require 'spec_helper'
RSpec.describe 'Abuse reports', :js, feature_category: :insider_threat do
- let_it_be(:abusive_user) { create(:user, :no_super_sidebar) }
+ let_it_be(:abusive_user) { create(:user) }
- let_it_be(:reporter1) { create(:user, :no_super_sidebar) }
+ let_it_be(:reporter1) { create(:user) }
let_it_be(:project) { create(:project, :public, :repository) }
let_it_be(:issue) { create(:issue, project: project, author: abusive_user) }
@@ -55,103 +55,53 @@ RSpec.describe 'Abuse reports', :js, feature_category: :insider_threat do
it_behaves_like 'reports the user with an abuse category'
end
- describe 'when user_profile_overflow_menu FF turned on' do
- context 'when reporting a user profile for abuse' do
- let_it_be(:reporter2) { create(:user, :no_super_sidebar) }
+ context 'when reporting a user profile for abuse' do
+ let_it_be(:reporter2) { create(:user) }
- before do
- visit user_path(abusive_user)
- find_by_testid('base-dropdown-toggle').click
- end
-
- it_behaves_like 'reports the user with an abuse category'
-
- it 'allows the reporter to report the same user for different abuse categories' do
- visit user_path(abusive_user)
-
- find_by_testid('base-dropdown-toggle').click
- fill_and_submit_abuse_category_form
- fill_and_submit_report_abuse_form
-
- expect(page).to have_content 'Thank you for your report'
-
- visit user_path(abusive_user)
-
- find_by_testid('base-dropdown-toggle').click
- fill_and_submit_abuse_category_form("They're being offensive or abusive.")
- fill_and_submit_report_abuse_form
-
- expect(page).to have_content 'Thank you for your report'
- end
-
- it 'allows multiple users to report the same user' do
- fill_and_submit_abuse_category_form
- fill_and_submit_report_abuse_form
-
- expect(page).to have_content 'Thank you for your report'
-
- gitlab_sign_out
- gitlab_sign_in(reporter2)
-
- visit user_path(abusive_user)
-
- find_by_testid('base-dropdown-toggle').click
- fill_and_submit_abuse_category_form
- fill_and_submit_report_abuse_form
-
- expect(page).to have_content 'Thank you for your report'
- end
-
- it_behaves_like 'cancel report'
+ before do
+ visit user_path(abusive_user)
+ find_by_testid('user-profile-actions').click
end
- end
-
- describe 'when user_profile_overflow_menu FF turned off' do
- context 'when reporting a user profile for abuse' do
- let_it_be(:reporter2) { create(:user, :no_super_sidebar) }
- before do
- stub_feature_flags(user_profile_overflow_menu_vue: false)
- visit user_path(abusive_user)
- end
-
- it_behaves_like 'reports the user with an abuse category'
-
- it 'allows the reporter to report the same user for different abuse categories' do
- visit user_path(abusive_user)
+ it_behaves_like 'reports the user with an abuse category'
- fill_and_submit_abuse_category_form
- fill_and_submit_report_abuse_form
+ it 'allows the reporter to report the same user for different abuse categories' do
+ visit user_path(abusive_user)
- expect(page).to have_content 'Thank you for your report'
+ find_by_testid('user-profile-actions').click
+ fill_and_submit_abuse_category_form
+ fill_and_submit_report_abuse_form
- visit user_path(abusive_user)
+ expect(page).to have_content 'Thank you for your report'
- fill_and_submit_abuse_category_form("They're being offensive or abusive.")
- fill_and_submit_report_abuse_form
+ visit user_path(abusive_user)
- expect(page).to have_content 'Thank you for your report'
- end
+ find_by_testid('user-profile-actions').click
+ fill_and_submit_abuse_category_form("They're being offensive or abusive.")
+ fill_and_submit_report_abuse_form
- it 'allows multiple users to report the same user' do
- fill_and_submit_abuse_category_form
- fill_and_submit_report_abuse_form
+ expect(page).to have_content 'Thank you for your report'
+ end
- expect(page).to have_content 'Thank you for your report'
+ it 'allows multiple users to report the same user', :js do
+ fill_and_submit_abuse_category_form
+ fill_and_submit_report_abuse_form
- gitlab_sign_out
- gitlab_sign_in(reporter2)
+ expect(page).to have_content 'Thank you for your report'
- visit user_path(abusive_user)
+ gitlab_sign_out
+ gitlab_sign_in(reporter2)
- fill_and_submit_abuse_category_form
- fill_and_submit_report_abuse_form
+ visit user_path(abusive_user)
- expect(page).to have_content 'Thank you for your report'
- end
+ find_by_testid('user-profile-actions').click
+ fill_and_submit_abuse_category_form
+ fill_and_submit_report_abuse_form
- it_behaves_like 'cancel report'
+ expect(page).to have_content 'Thank you for your report'
end
+
+ it_behaves_like 'cancel report'
end
context 'when reporting an merge request for abuse' do
@@ -180,10 +130,6 @@ RSpec.describe 'Abuse reports', :js, feature_category: :insider_threat do
end
end
- # TODO: implement tests before the FF "user_profile_overflow_menu_vue" is turned on
- # See: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/122971
- # Related Issue: https://gitlab.com/gitlab-org/gitlab/-/issues/416983
-
private
def fill_and_submit_abuse_category_form(category = "They're posting spam.")
diff --git a/spec/features/admin/admin_browse_spam_logs_spec.rb b/spec/features/admin/admin_browse_spam_logs_spec.rb
index c272a8630b7..f781e2adf07 100644
--- a/spec/features/admin/admin_browse_spam_logs_spec.rb
+++ b/spec/features/admin/admin_browse_spam_logs_spec.rb
@@ -4,9 +4,9 @@ require 'spec_helper'
RSpec.describe 'Admin browse spam logs', feature_category: :shared do
let!(:spam_log) { create(:spam_log, description: 'abcde ' * 20) }
+ let(:admin) { create(:admin) }
before do
- admin = create(:admin)
sign_in(admin)
gitlab_enable_admin_mode_sign_in(admin)
end
@@ -22,6 +22,7 @@ RSpec.describe 'Admin browse spam logs', feature_category: :shared do
expect(page).to have_content("#{spam_log.description[0...97]}...")
expect(page).to have_link('Remove user')
expect(page).to have_link('Block user')
+ expect(page).to have_link('Trust user')
end
it 'does not perform N+1 queries' do
@@ -30,4 +31,15 @@ RSpec.describe 'Admin browse spam logs', feature_category: :shared do
expect { visit admin_spam_logs_path }.not_to exceed_query_limit(control_queries)
end
+
+ context 'when user is trusted' do
+ before do
+ UserCustomAttribute.set_trusted_by(user: spam_log.user, trusted_by: admin)
+ end
+
+ it 'allows admin to untrust the user' do
+ visit admin_spam_logs_path
+ expect(page).to have_link('Untrust user')
+ end
+ end
end
diff --git a/spec/features/admin/admin_deploy_keys_spec.rb b/spec/features/admin/admin_deploy_keys_spec.rb
index f59b4db5cc2..f9510ef296a 100644
--- a/spec/features/admin/admin_deploy_keys_spec.rb
+++ b/spec/features/admin/admin_deploy_keys_spec.rb
@@ -18,7 +18,7 @@ RSpec.describe 'admin deploy keys', :js, feature_category: :system_access do
it 'show all public deploy keys' do
visit admin_deploy_keys_path
- page.within(find('[data-testid="deploy-keys-list"]', match: :first)) do
+ within_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
@@ -29,7 +29,7 @@ RSpec.describe 'admin deploy keys', :js, feature_category: :system_access do
visit admin_deploy_keys_path
- page.within(find('[data-testid="deploy-keys-list"]', match: :first)) do
+ within_testid('deploy-keys-list', match: :first) do
expect(page).to have_content(write_key.project.full_name)
end
end
@@ -49,7 +49,7 @@ RSpec.describe 'admin deploy keys', :js, feature_category: :system_access do
expect(page).to have_current_path admin_deploy_keys_path, ignore_query: true
- page.within(find('[data-testid="deploy-keys-list"]', match: :first)) do
+ within_testid('deploy-keys-list', match: :first) do
expect(page).to have_content('laptop')
end
end
@@ -69,7 +69,7 @@ RSpec.describe 'admin deploy keys', :js, feature_category: :system_access do
expect(page).to have_current_path admin_deploy_keys_path, ignore_query: true
- page.within(find('[data-testid="deploy-keys-list"]', match: :first)) do
+ within_testid('deploy-keys-list', match: :first) do
expect(page).to have_content('new-title')
end
end
@@ -88,7 +88,7 @@ RSpec.describe 'admin deploy keys', :js, feature_category: :system_access do
end
expect(page).to have_current_path admin_deploy_keys_path, ignore_query: true
- page.within(find('[data-testid="deploy-keys-list"]', match: :first)) do
+ within_testid('deploy-keys-list', match: :first) do
expect(page).not_to have_content(deploy_key.title)
end
end
diff --git a/spec/features/admin/admin_dev_ops_reports_spec.rb b/spec/features/admin/admin_dev_ops_reports_spec.rb
index f290464b043..99d43e6b0da 100644
--- a/spec/features/admin/admin_dev_ops_reports_spec.rb
+++ b/spec/features/admin/admin_dev_ops_reports_spec.rb
@@ -19,8 +19,8 @@ RSpec.describe 'DevOps Report page', :js, feature_category: :devops_reports do
expect(page).to have_content 'Introducing Your DevOps Report'
- page.within(find('[data-testid="devops-score-container"]')) do
- find('[data-testid="close-icon"]').click
+ within_testid('devops-score-container') do
+ find_by_testid('close-icon').click
end
expect(page).not_to have_content 'Introducing Your DevOps Report'
diff --git a/spec/features/admin/admin_groups_spec.rb b/spec/features/admin/admin_groups_spec.rb
index 1e3dbd7fea4..f071da1835a 100644
--- a/spec/features/admin/admin_groups_spec.rb
+++ b/spec/features/admin/admin_groups_spec.rb
@@ -138,7 +138,7 @@ RSpec.describe 'Admin Groups', feature_category: :groups_and_projects do
it 'shows access requests with link to manage access' do
visit admin_group_path(group)
- page.within '[data-testid="access-requests"]' do
+ within_testid('access-requests') do
expect(page).to have_content access_request.user.name
expect(page).to have_link 'Manage access', href: group_group_members_path(group, tab: 'access_requests')
end
diff --git a/spec/features/admin/admin_hooks_spec.rb b/spec/features/admin/admin_hooks_spec.rb
index a5acba1fe4a..2aec5baf351 100644
--- a/spec/features/admin/admin_hooks_spec.rb
+++ b/spec/features/admin/admin_hooks_spec.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe 'Admin::Hooks', feature_category: :webhooks do
include Spec::Support::Helpers::ModalHelpers
- let_it_be(:user) { create(:admin, :no_super_sidebar) }
+ let_it_be(:user) { create(:admin) }
before do
sign_in(user)
@@ -13,10 +13,10 @@ RSpec.describe 'Admin::Hooks', feature_category: :webhooks do
end
describe 'GET /admin/hooks' do
- it 'is ok' do
+ it 'is ok', :js do
visit admin_root_path
- page.within '.nav-sidebar' do
+ within_testid('super-sidebar') do
click_on 'System Hooks', match: :first
end
diff --git a/spec/features/admin/admin_jobs_spec.rb b/spec/features/admin/admin_jobs_spec.rb
index b125974532b..b3e21d02354 100644
--- a/spec/features/admin/admin_jobs_spec.rb
+++ b/spec/features/admin/admin_jobs_spec.rb
@@ -132,7 +132,7 @@ RSpec.describe 'Admin Jobs', :js, feature_category: :continuous_integration do
within_testid('jobs-table') do
expect(page).to have_selector('[data-testid="jobs-table-row"]', count: 1)
- expect(find_by_testid('ci-badge-text')).to have_content('Failed')
+ expect(find_by_testid('ci-icon-text')).to have_content('Failed')
end
end
end
diff --git a/spec/features/admin/admin_mode/logout_spec.rb b/spec/features/admin/admin_mode/logout_spec.rb
index 5d9106fea02..7a33256e7a8 100644
--- a/spec/features/admin/admin_mode/logout_spec.rb
+++ b/spec/features/admin/admin_mode/logout_spec.rb
@@ -5,9 +5,8 @@ require 'spec_helper'
RSpec.describe 'Admin Mode Logout', :js, feature_category: :system_access do
include TermsHelper
include UserLoginHelper
- include Features::TopNavSpecHelpers
- let(:user) { create(:admin, :no_super_sidebar) }
+ let(:user) { create(:admin) }
before do
# TODO: This used to use gitlab_sign_in, instead of sign_in, but that is buggy. See
@@ -22,11 +21,9 @@ RSpec.describe 'Admin Mode Logout', :js, feature_category: :system_access do
expect(page).to have_current_path root_path, ignore_query: true
- open_top_nav
+ click_button 'Search or go to…'
- within_top_nav do
- expect(page).to have_link(href: new_admin_session_path)
- end
+ expect(page).to have_link(href: new_admin_session_path)
end
it 'disable shows flash notice' do
@@ -45,11 +42,9 @@ RSpec.describe 'Admin Mode Logout', :js, feature_category: :system_access do
expect(page).to have_current_path root_path, ignore_query: true
- open_top_nav
+ click_button 'Search or go to…'
- within_top_nav do
- expect(page).to have_link(href: new_admin_session_path)
- end
+ expect(page).to have_link(href: new_admin_session_path)
end
end
end
diff --git a/spec/features/admin/admin_mode/workers_spec.rb b/spec/features/admin/admin_mode/workers_spec.rb
index 2a862c750d7..124c43eef9d 100644
--- a/spec/features/admin/admin_mode/workers_spec.rb
+++ b/spec/features/admin/admin_mode/workers_spec.rb
@@ -6,8 +6,8 @@ require 'spec_helper'
RSpec.describe 'Admin mode for workers', :request_store, feature_category: :system_access do
include Features::AdminUsersHelpers
- let(:user) { create(:user, :no_super_sidebar) }
- let(:user_to_delete) { create(:user, :no_super_sidebar) }
+ let(:user) { create(:user) }
+ let(:user_to_delete) { create(:user) }
before do
sign_in(user)
@@ -22,7 +22,7 @@ RSpec.describe 'Admin mode for workers', :request_store, feature_category: :syst
end
context 'as an admin user' do
- let(:user) { create(:admin, :no_super_sidebar) }
+ let(:user) { create(:admin) }
context 'when admin mode disabled' do
it 'cannot delete user', :js do
diff --git a/spec/features/admin/admin_mode_spec.rb b/spec/features/admin/admin_mode_spec.rb
index edfa58567ad..b1b44ce143f 100644
--- a/spec/features/admin/admin_mode_spec.rb
+++ b/spec/features/admin/admin_mode_spec.rb
@@ -4,10 +4,9 @@ require 'spec_helper'
RSpec.describe 'Admin mode', :js, feature_category: :shared do
include MobileHelpers
- include Features::TopNavSpecHelpers
include StubENV
- let(:admin) { create(:admin, :no_super_sidebar) }
+ let(:admin) { create(:admin) }
before do
stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false')
@@ -21,20 +20,16 @@ RSpec.describe 'Admin mode', :js, feature_category: :shared do
context 'when not in admin mode' do
it 'has no leave admin mode button' do
visit new_admin_session_path
- open_top_nav
+ open_search_modal
- page.within('.navbar-sub-nav') do
- expect(page).not_to have_link(href: destroy_admin_session_path)
- end
+ expect(page).not_to have_link(href: destroy_admin_session_path)
end
it 'can open pages not in admin scope' do
visit new_admin_session_path
- open_top_nav_projects
+ open_search_modal
- within_top_nav do
- click_link('View all projects')
- end
+ click_link('View all my projects')
expect(page).to have_current_path(dashboard_projects_path)
end
@@ -78,29 +73,23 @@ RSpec.describe 'Admin mode', :js, feature_category: :shared do
end
it 'contains link to leave admin mode' do
- open_top_nav
+ open_search_modal
- within_top_nav do
- expect(page).to have_link(href: destroy_admin_session_path)
- end
+ expect(page).to have_link(href: destroy_admin_session_path)
end
it 'can leave admin mode using main dashboard link' do
gitlab_disable_admin_mode
- open_top_nav
+ open_search_modal
- within_top_nav do
- expect(page).to have_link(href: new_admin_session_path)
- end
+ expect(page).to have_link(href: new_admin_session_path)
end
it 'can open pages not in admin scope' do
- open_top_nav_projects
+ open_search_modal
- within_top_nav do
- click_link('View all projects')
- end
+ click_link('View all my projects')
expect(page).to have_current_path(dashboard_projects_path)
end
@@ -108,7 +97,7 @@ RSpec.describe 'Admin mode', :js, feature_category: :shared do
context 'nav bar' do
it 'shows admin dashboard links on bigger screen' do
visit root_dashboard_path
- open_top_nav
+ open_search_modal
expect(page).to have_link(text: 'Admin', href: admin_root_path, visible: true)
expect(page).to have_link(text: 'Leave admin mode', href: destroy_admin_session_path, visible: true)
@@ -123,11 +112,9 @@ RSpec.describe 'Admin mode', :js, feature_category: :shared do
it 'can leave admin mode' do
gitlab_disable_admin_mode
- open_top_nav
+ open_search_modal
- within_top_nav do
- expect(page).to have_link(href: new_admin_session_path)
- end
+ expect(page).to have_link(href: new_admin_session_path)
end
end
end
@@ -141,10 +128,14 @@ RSpec.describe 'Admin mode', :js, feature_category: :shared do
it 'shows no admin mode buttons in navbar' do
visit admin_root_path
- open_top_nav
+ open_search_modal
expect(page).not_to have_link(href: new_admin_session_path)
expect(page).not_to have_link(href: destroy_admin_session_path)
end
end
+
+ def open_search_modal
+ click_button 'Search or go to…'
+ end
end
diff --git a/spec/features/admin/admin_projects_spec.rb b/spec/features/admin/admin_projects_spec.rb
index 3454b7af962..b793299e253 100644
--- a/spec/features/admin/admin_projects_spec.rb
+++ b/spec/features/admin/admin_projects_spec.rb
@@ -95,7 +95,7 @@ RSpec.describe "Admin::Projects", feature_category: :groups_and_projects do
context 'when project has open access requests' do
it 'shows access requests with link to manage access' do
- page.within '[data-testid="access-requests"]' do
+ within_testid('access-requests') do
expect(page).to have_content access_request.user.name
expect(page).to have_link 'Manage access', href: project_project_members_path(project, tab: 'access_requests')
end
diff --git a/spec/features/admin/admin_runners_spec.rb b/spec/features/admin/admin_runners_spec.rb
index 9edd970532e..750f5f8d4b9 100644
--- a/spec/features/admin/admin_runners_spec.rb
+++ b/spec/features/admin/admin_runners_spec.rb
@@ -100,7 +100,7 @@ RSpec.describe "Admin Runners", feature_category: :runner_fleet do
visit admin_runners_path
within_runner_row(runner.id) do
- expect(find("[data-testid='job-count']")).to have_content '2'
+ expect(find_by_testid('job-count')).to have_content '2'
end
end
@@ -116,8 +116,8 @@ RSpec.describe "Admin Runners", feature_category: :runner_fleet do
expect(current_url).to match(admin_runner_path(runner))
- expect(find("[data-testid='td-status']")).to have_content "Running"
- expect(find("[data-testid='td-job']")).to have_content "##{job.id}"
+ expect(find_by_testid('td-status')).to have_content "Running"
+ expect(find_by_testid('td-job')).to have_content "##{job.id}"
end
describe 'search' do
@@ -202,6 +202,36 @@ RSpec.describe "Admin Runners", feature_category: :runner_fleet do
end
end
+ describe 'filter by version prefix' do
+ before_all do
+ runner_v15 = create(:ci_runner, :instance, description: 'runner-v15')
+ runner_v14 = create(:ci_runner, :instance, description: 'runner-v14')
+
+ create(:ci_runner_machine, runner: runner_v15, version: '15.0.0')
+ create(:ci_runner_machine, runner: runner_v14, version: '14.0.0')
+ end
+
+ before do
+ visit admin_runners_path
+ end
+
+ it 'shows all runners' do
+ expect(page).to have_link('All 2')
+
+ expect(page).to have_content 'runner-v15'
+ expect(page).to have_content 'runner-v14'
+ end
+
+ it 'shows filtered runner based on supplied prefix' do
+ input_filtered_search_filter_is_only(s_('Runners|Version starts with'), '15.0')
+
+ expect(page).to have_link('All 1')
+
+ expect(page).not_to have_content 'runner-v14'
+ expect(page).to have_content 'runner-v15'
+ end
+ end
+
describe 'filter by status' do
let_it_be(:never_contacted) do
create(:ci_runner, :instance, description: 'runner-never-contacted', contacted_at: nil)
@@ -291,7 +321,7 @@ RSpec.describe "Admin Runners", feature_category: :runner_fleet do
expect(page).to have_link('Group 1')
expect(page).to have_link('Project 1')
- page.within('[data-testid="runner-type-tabs"]') do
+ within_testid('runner-type-tabs') do
expect(page).to have_link('All', class: 'active')
end
end
@@ -302,7 +332,7 @@ RSpec.describe "Admin Runners", feature_category: :runner_fleet do
expect(page).to have_content 'runner-project'
expect(page).to have_content 'runner-group'
- page.within('[data-testid="runner-type-tabs"]') do
+ within_testid('runner-type-tabs') do
click_on('Project')
expect(page).to have_link('Project', class: 'active')
@@ -315,7 +345,7 @@ RSpec.describe "Admin Runners", feature_category: :runner_fleet do
it 'show the same counts after selecting another tab' do
visit admin_runners_path
- page.within('[data-testid="runner-type-tabs"]') do
+ within_testid('runner-type-tabs') do
click_on('Project')
expect(page).to have_link('All 2')
@@ -329,7 +359,7 @@ RSpec.describe "Admin Runners", feature_category: :runner_fleet do
visit admin_runners_path
- page.within('[data-testid="runner-type-tabs"]') do
+ within_testid('runner-type-tabs') do
click_on 'Project'
end
@@ -355,7 +385,7 @@ RSpec.describe "Admin Runners", feature_category: :runner_fleet do
expect(page).to have_content 'runner-group'
expect(page).not_to have_content 'runner-paused-project'
- page.within('[data-testid="runner-type-tabs"]') do
+ within_testid('runner-type-tabs') do
click_on 'Project'
end
@@ -367,7 +397,7 @@ RSpec.describe "Admin Runners", feature_category: :runner_fleet do
context 'when type does not match' do
before do
visit admin_runners_path
- page.within('[data-testid="runner-type-tabs"]') do
+ within_testid('runner-type-tabs') do
click_on 'Instance'
end
end
@@ -440,24 +470,28 @@ RSpec.describe "Admin Runners", feature_category: :runner_fleet do
visit admin_runners_path
- within '[data-testid="runner-list"] tbody tr:nth-child(1)' do
- expect(page).to have_content 'runner-2'
- end
+ within_testid('runner-list') do
+ within('tbody tr:nth-child(1)') do
+ expect(page).to have_content 'runner-2'
+ end
- within '[data-testid="runner-list"] tbody tr:nth-child(2)' do
- expect(page).to have_content 'runner-1'
+ within('tbody tr:nth-child(2)') do
+ expect(page).to have_content 'runner-1'
+ end
end
click_on 'Created date' # Open "sort by" dropdown
click_on 'Last contact'
click_on 'Sort direction: Descending'
- within '[data-testid="runner-list"] tbody tr:nth-child(1)' do
- expect(page).to have_content 'runner-1'
- end
+ within_testid('runner-list') do
+ within('tbody tr:nth-child(1)') do
+ expect(page).to have_content 'runner-1'
+ end
- within '[data-testid="runner-list"] tbody tr:nth-child(2)' do
- expect(page).to have_content 'runner-2'
+ within('tbody tr:nth-child(2)') do
+ expect(page).to have_content 'runner-2'
+ end
end
end
end
@@ -522,8 +556,8 @@ RSpec.describe "Admin Runners", feature_category: :runner_fleet do
describe 'runner show page breadcrumbs' do
it 'contains the current runner id and token' do
- page.within '[data-testid="breadcrumb-links"]' do
- expect(page.find('[data-testid="breadcrumb-current-link"]')).to have_link(
+ within_testid('breadcrumb-links') do
+ expect(find_by_testid('breadcrumb-current-link')).to have_link(
"##{runner.id} (#{runner.short_sha})"
)
end
@@ -555,7 +589,7 @@ RSpec.describe "Admin Runners", feature_category: :runner_fleet do
end
it 'deletes runner and redirects to runner list' do
- expect(page.find('[data-testid="alert-success"]')).to have_content('deleted')
+ expect(find_by_testid('alert-success')).to have_content('deleted')
expect(current_url).to match(admin_runners_path)
end
end
@@ -581,9 +615,9 @@ RSpec.describe "Admin Runners", feature_category: :runner_fleet do
describe 'breadcrumbs' do
it 'contains the current runner id and token' do
- page.within '[data-testid="breadcrumb-links"]' do
+ within_testid('breadcrumb-links') do
expect(page).to have_link("##{project_runner.id} (#{project_runner.short_sha})")
- expect(page.find('[data-testid="breadcrumb-current-link"]')).to have_content("Edit")
+ expect(find_by_testid('breadcrumb-current-link')).to have_content("Edit")
end
end
end
@@ -591,7 +625,7 @@ RSpec.describe "Admin Runners", feature_category: :runner_fleet do
describe 'runner header', :js do
it 'contains the runner status, type and id' do
expect(page).to have_content(
- "##{project_runner.id} (#{project_runner.short_sha}) #{s_('Runners|Never contacted')} Project created"
+ "##{project_runner.id} (#{project_runner.short_sha}) #{s_('Runners|Never contacted')} Project Created"
)
end
end
@@ -604,7 +638,7 @@ RSpec.describe "Admin Runners", feature_category: :runner_fleet do
it 'show success alert and redirects to runner page' do
expect(current_url).to match(admin_runner_path(project_runner))
- expect(page.find('[data-testid="alert-success"]')).to have_content('saved')
+ expect(find_by_testid('alert-success')).to have_content('saved')
end
end
@@ -631,11 +665,13 @@ RSpec.describe "Admin Runners", feature_category: :runner_fleet do
describe 'enable/create' do
shared_examples 'assignable runner' do
it 'enables a runner for a project' do
- within find('[data-testid="unassigned-projects"] tr', text: project2.full_name) do
- click_on 'Enable'
+ within_testid('unassigned-projects') do
+ within('tr', text: project2.full_name) do
+ click_on 'Enable'
+ end
end
- assigned_project = page.find('[data-testid="assigned-projects"]')
+ assigned_project = find_by_testid('assigned-projects')
expect(page).to have_content('Runner assigned to project.')
expect(assigned_project).to have_content(project2.name)
@@ -671,11 +707,11 @@ RSpec.describe "Admin Runners", feature_category: :runner_fleet do
end
it 'removed project runner from project' do
- within '[data-testid="assigned-projects"]' do
+ within_testid('assigned-projects') do
click_on 'Disable'
end
- new_runner_project = page.find('[data-testid="unassigned-projects"]')
+ new_runner_project = find_by_testid('unassigned-projects')
expect(page).to have_content('Runner unassigned from project.')
expect(new_runner_project).to have_content(project1.name)
diff --git a/spec/features/admin/admin_sees_background_migrations_spec.rb b/spec/features/admin/admin_sees_background_migrations_spec.rb
index 7423e74bf3a..ae307b8038c 100644
--- a/spec/features/admin/admin_sees_background_migrations_spec.rb
+++ b/spec/features/admin/admin_sees_background_migrations_spec.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe "Admin > Admin sees background migrations", feature_category: :database do
include ListboxHelpers
- let_it_be(:admin) { create(:admin, :no_super_sidebar) }
+ let_it_be(:admin) { create(:admin) }
let(:job_class) { Gitlab::BackgroundMigration::CopyColumnUsingBackgroundMigrationJob }
let_it_be(:active_migration) { create(:batched_background_migration, :active, table_name: 'active') }
@@ -21,16 +21,18 @@ RSpec.describe "Admin > Admin sees background migrations", feature_category: :da
gitlab_enable_admin_mode_sign_in(admin)
end
- it 'can navigate to background migrations' do
+ it 'can navigate to background migrations', :js do
visit admin_root_path
- within '.nav-sidebar' do
- link = find_link 'Background Migrations'
+ within_testid('super-sidebar') do
+ click_on 'Monitoring'
+ click_on 'Background Migrations'
+ end
- link.click
+ expect(page).to have_current_path(admin_background_migrations_path)
- expect(page).to have_current_path(admin_background_migrations_path)
- expect(link).to have_ancestor(:css, 'li.active')
+ within_testid('super-sidebar') do
+ expect(page).to have_css('a[aria-current="page"]', text: 'Background Migrations')
end
end
diff --git a/spec/features/admin/admin_settings_spec.rb b/spec/features/admin/admin_settings_spec.rb
index 1b10ea81333..4e0198b1f2b 100644
--- a/spec/features/admin/admin_settings_spec.rb
+++ b/spec/features/admin/admin_settings_spec.rb
@@ -7,7 +7,7 @@ RSpec.describe 'Admin updates settings', feature_category: :shared do
include TermsHelper
include UsageDataHelpers
- let_it_be(:admin) { create(:admin, :no_super_sidebar) }
+ let_it_be(:admin) { create(:admin) }
context 'application setting :admin_mode is enabled', :request_store do
before do
@@ -22,7 +22,7 @@ RSpec.describe 'Admin updates settings', feature_category: :shared do
end
it 'change visibility settings' do
- page.within('[data-testid="admin-visibility-access-settings"]') do
+ within_testid('admin-visibility-access-settings') do
choose "application_setting_default_project_visibility_20"
click_button 'Save changes'
end
@@ -31,19 +31,19 @@ RSpec.describe 'Admin updates settings', feature_category: :shared do
end
it 'uncheck all restricted visibility levels' do
- page.within('[data-testid="restricted-visibility-levels"]') do
+ within_testid('restricted-visibility-levels') do
uncheck s_('VisibilityLevel|Public')
uncheck s_('VisibilityLevel|Internal')
uncheck s_('VisibilityLevel|Private')
end
- page.within('[data-testid="admin-visibility-access-settings"]') do
+ within_testid('admin-visibility-access-settings') do
click_button 'Save changes'
end
expect(page).to have_content "Application settings saved successfully"
- page.within('[data-testid="restricted-visibility-levels"]') do
+ within_testid('restricted-visibility-levels') do
expect(find_field(s_('VisibilityLevel|Public'))).not_to be_checked
expect(find_field(s_('VisibilityLevel|Internal'))).not_to be_checked
expect(find_field(s_('VisibilityLevel|Private'))).not_to be_checked
@@ -53,7 +53,7 @@ RSpec.describe 'Admin updates settings', feature_category: :shared do
it 'modify import sources' do
expect(current_settings.import_sources).to be_empty
- page.within('[data-testid="admin-import-export-settings"]') do
+ within_testid('admin-import-export-settings') do
check "Repository by URL"
click_button 'Save changes'
end
@@ -63,12 +63,12 @@ RSpec.describe 'Admin updates settings', feature_category: :shared do
end
it 'change Visibility and Access Controls' do
- page.within('[data-testid="admin-import-export-settings"]') do
- page.within('[data-testid="project-export"]') do
+ within_testid('admin-import-export-settings') do
+ within_testid('project-export') do
uncheck 'Enabled'
end
- page.within('[data-testid="bulk-import"]') do
+ within_testid('bulk-import') do
check 'Enabled'
end
@@ -81,7 +81,7 @@ RSpec.describe 'Admin updates settings', feature_category: :shared do
end
it 'change Keys settings' do
- page.within('[data-testid="admin-visibility-access-settings"]') do
+ within_testid('admin-visibility-access-settings') do
select 'Are forbidden', from: 'RSA SSH keys'
select 'Are allowed', from: 'DSA SSH keys'
select 'Must be at least 384 bits', from: 'ECDSA SSH keys'
@@ -103,7 +103,7 @@ RSpec.describe 'Admin updates settings', feature_category: :shared do
end
it 'change Account and Limit Settings' do
- page.within(find('[data-testid="account-and-limit-settings-content"]')) do
+ within_testid('account-and-limit-settings-content') do
uncheck 'Gravatar enabled'
click_button 'Save changes'
end
@@ -113,7 +113,7 @@ RSpec.describe 'Admin updates settings', feature_category: :shared do
end
it 'change Maximum export size' do
- page.within(find('[data-testid="admin-import-export-settings"]')) do
+ within_testid('admin-import-export-settings') do
fill_in 'Maximum export size (MiB)', with: 25
click_button 'Save changes'
end
@@ -123,7 +123,7 @@ RSpec.describe 'Admin updates settings', feature_category: :shared do
end
it 'change Maximum import size' do
- page.within(find('[data-testid="admin-import-export-settings"]')) do
+ within_testid('admin-import-export-settings') do
fill_in 'Maximum import size (MiB)', with: 15
click_button 'Save changes'
end
@@ -169,7 +169,7 @@ RSpec.describe 'Admin updates settings', feature_category: :shared do
expect(page).to have_unchecked_field(_('Deactivate dormant users after a period of inactivity'))
expect(current_settings.deactivate_dormant_users).to be_falsey
- page.within(find('[data-testid="account-and-limit-settings-content"]')) do
+ within_testid('account-and-limit-settings-content') do
check _('Deactivate dormant users after a period of inactivity')
click_button _('Save changes')
end
@@ -185,7 +185,7 @@ RSpec.describe 'Admin updates settings', feature_category: :shared do
it 'change dormant users period', :js do
expect(page).to have_field(_('Days of inactivity before deactivation'), disabled: true)
- page.within(find('[data-testid="account-and-limit-settings-content"]')) do
+ within_testid('account-and-limit-settings-content') do
check _('Deactivate dormant users after a period of inactivity')
fill_in _('Days of inactivity before deactivation'), with: '180'
click_button _('Save changes')
@@ -202,7 +202,7 @@ RSpec.describe 'Admin updates settings', feature_category: :shared do
selector = '#application_setting_deactivate_dormant_users_period_error'
expect(page).not_to have_selector(selector, visible: :visible)
- page.within(find('[data-testid="account-and-limit-settings-content"]')) do
+ within_testid('account-and-limit-settings-content') do
check 'application_setting_deactivate_dormant_users'
fill_in _('application_setting_deactivate_dormant_users_period'), with: '30'
click_button 'Save changes'
@@ -666,28 +666,47 @@ RSpec.describe 'Admin updates settings', feature_category: :shared do
expect(find_field('Allow access to members of the following group').value).to be_nil
end
- it 'loads togglable usage ping payload on click', :js do
- allow(Gitlab::Usage::ServicePingReport).to receive(:for).and_return({ uuid: '12345678', hostname: '127.0.0.1' })
+ context 'Service usage data', :with_license do
+ before do
+ stub_usage_data_connections
+ stub_database_flavor_check
+ end
- stub_usage_data_connections
- stub_database_flavor_check
+ context 'when service data cached' do
+ before_all do
+ create(:raw_usage_data)
+ end
- page.within('#js-usage-settings') do
- expected_payload_content = /(?=.*"uuid")(?=.*"hostname")/m
+ it 'loads usage ping payload on click', :js do
+ expected_payload_content = /(?=.*"test")/m
- expect(page).not_to have_content expected_payload_content
+ expect(page).not_to have_content expected_payload_content
- click_button('Preview payload')
+ click_button('Preview payload')
- wait_for_requests
+ wait_for_requests
+
+ expect(page).to have_button 'Hide payload'
+ expect(page).to have_content expected_payload_content
+ end
- expect(page).to have_selector '.js-service-ping-payload'
- expect(page).to have_button 'Hide payload'
- expect(page).to have_content expected_payload_content
+ it 'generates usage ping payload on button click', :js do
+ expect_next_instance_of(Admin::ApplicationSettingsController) do |instance|
+ expect(instance).to receive(:usage_data).and_call_original
+ end
- click_button('Hide payload')
+ click_button('Download payload')
- expect(page).not_to have_content expected_payload_content
+ wait_for_requests
+ end
+ end
+
+ context 'when service data not cached' do
+ it 'renders missing cache information' do
+ visit metrics_and_profiling_admin_application_settings_path
+
+ expect(page).to have_text('Service Ping payload not found in the application cache')
+ end
end
end
end
@@ -799,7 +818,7 @@ RSpec.describe 'Admin updates settings', feature_category: :shared do
it 'changes gitlab shell operation limits settings' do
visit network_admin_application_settings_path
- page.within('[data-testid="gitlab-shell-operation-limits"]') do
+ within_testid('gitlab-shell-operation-limits') do
fill_in 'Maximum number of Git operations per minute', with: 100
click_button 'Save changes'
end
@@ -971,15 +990,14 @@ RSpec.describe 'Admin updates settings', feature_category: :shared do
end
end
- context 'Nav bar' do
+ context 'Nav bar', :js do
it 'shows default help links in nav' do
default_support_url = "https://#{ApplicationHelper.promo_host}/get-help/"
visit root_dashboard_path
- find('.header-help-dropdown-toggle').click
-
- page.within '.header-help' do
+ within_testid('super-sidebar') do
+ click_on 'Help'
expect(page).to have_link(text: 'Help', href: help_path)
expect(page).to have_link(text: 'Support', href: default_support_url)
end
@@ -991,68 +1009,12 @@ RSpec.describe 'Admin updates settings', feature_category: :shared do
visit root_dashboard_path
- find('.header-help-dropdown-toggle').click
-
- page.within '.header-help' do
+ within_testid('super-sidebar') do
+ click_on 'Help'
expect(page).to have_link(text: 'Support', href: new_support_url)
end
end
end
-
- context 'Service usage data page', :with_license do
- before do
- stub_usage_data_connections
- stub_database_flavor_check
- end
-
- context 'when service data cached', :use_clean_rails_memory_store_caching do
- let(:usage_data) { { uuid: "1111", hostname: "localhost", counts: { issue: 0 } }.deep_stringify_keys }
-
- before do
- # We are mocking Gitlab::Usage::ServicePingReport because this dataset generation
- # takes a very long time, and is not what we're testing in this context.
- #
- # See https://gitlab.com/gitlab-org/gitlab/-/issues/414929
- allow(Gitlab::UsageData).to receive(:data).and_return(usage_data)
- allow(Gitlab::Usage::ServicePingReport).to receive(:with_instrumentation_classes)
- .with(usage_data, :with_value).and_return(usage_data)
-
- visit usage_data_admin_application_settings_path
- visit service_usage_data_admin_application_settings_path
- end
-
- it 'loads usage ping payload on click', :js do
- expected_payload_content = /(?=.*"uuid")(?=.*"hostname")/m
-
- expect(page).not_to have_content expected_payload_content
-
- click_button('Preview payload')
-
- wait_for_requests
-
- expect(page).to have_button 'Hide payload'
- expect(page).to have_content expected_payload_content
- end
-
- it 'generates usage ping payload on button click', :js do
- expect_next_instance_of(Admin::ApplicationSettingsController) do |instance|
- expect(instance).to receive(:usage_data).and_call_original
- end
-
- click_button('Download payload')
-
- wait_for_requests
- end
- end
-
- context 'when service data not cached' do
- it 'renders missing cache information' do
- visit service_usage_data_admin_application_settings_path
-
- expect(page).to have_text('Service Ping payload not found in the application cache')
- end
- end
- end
end
context 'application setting :admin_mode is disabled' do
diff --git a/spec/features/admin/admin_users_spec.rb b/spec/features/admin/admin_users_spec.rb
index ca08bc9e577..9ab5b1fd3bb 100644
--- a/spec/features/admin/admin_users_spec.rb
+++ b/spec/features/admin/admin_users_spec.rb
@@ -82,4 +82,12 @@ RSpec.describe "Admin::Users", feature_category: :user_management do
end
end
end
+
+ it 'does not perform N+1 queries' do
+ control_queries = ActiveRecord::QueryRecorder.new { visit admin_users_path }
+
+ expect { create(:user) }.to change { User.count }.by(1)
+
+ expect { visit admin_users_path }.not_to exceed_query_limit(control_queries)
+ end
end
diff --git a/spec/features/admin/admin_uses_repository_checks_spec.rb b/spec/features/admin/admin_uses_repository_checks_spec.rb
index d9d36ec3bae..05232de35e5 100644
--- a/spec/features/admin/admin_uses_repository_checks_spec.rb
+++ b/spec/features/admin/admin_uses_repository_checks_spec.rb
@@ -47,7 +47,7 @@ RSpec.describe 'Admin uses repository checks', :request_store, feature_category:
)
visit_admin_project_page(project)
- page.within('[data-testid="last-repository-check-failed-alert"]') do
+ within_testid('last-repository-check-failed-alert') do
expect(page.text).to match(/Last repository check \(just now\) failed/)
end
end
diff --git a/spec/features/admin/broadcast_messages_spec.rb b/spec/features/admin/broadcast_messages_spec.rb
index b89ebc34d6a..e4a2e31ee1c 100644
--- a/spec/features/admin/broadcast_messages_spec.rb
+++ b/spec/features/admin/broadcast_messages_spec.rb
@@ -36,12 +36,12 @@ RSpec.describe 'Admin Broadcast Messages', :js, feature_category: :onboarding do
# edit
page.within(first_message_container) do
- find('[data-testid="edit-message"]').click
+ find_by_testid('edit-message').click
end
wait_for_requests
- expect(find('[data-testid="message-input"]').value).to eq('test message')
+ expect(find_by_testid('message-input').value).to eq('test message')
fill_in 'Message', with: 'changed test message'
@@ -61,11 +61,11 @@ RSpec.describe 'Admin Broadcast Messages', :js, feature_category: :onboarding do
end
def preview_container
- find('[data-testid="preview-broadcast-message"]')
+ find_by_testid('preview-broadcast-message')
end
def first_message_container
- find('[data-testid="message-row"]', match: :first)
+ find_by_testid('message-row', match: :first)
end
end
end
diff --git a/spec/features/admin/users/user_spec.rb b/spec/features/admin/users/user_spec.rb
index 7dc329e6909..b8dc725c17f 100644
--- a/spec/features/admin/users/user_spec.rb
+++ b/spec/features/admin/users/user_spec.rb
@@ -6,8 +6,8 @@ RSpec.describe 'Admin::Users::User', feature_category: :user_management do
include Features::AdminUsersHelpers
include Spec::Support::Helpers::ModalHelpers
- let_it_be(:user) { create(:omniauth_user, :no_super_sidebar, provider: 'twitter', extern_uid: '123456') }
- let_it_be(:current_user) { create(:admin, :no_super_sidebar) }
+ let_it_be(:user) { create(:omniauth_user, provider: 'twitter', extern_uid: '123456') }
+ let_it_be(:current_user) { create(:admin) }
before do
sign_in(current_user)
@@ -145,7 +145,7 @@ RSpec.describe 'Admin::Users::User', feature_category: :user_management do
end
describe 'Impersonation' do
- let_it_be(:another_user) { create(:user, :no_super_sidebar) }
+ let_it_be(:another_user) { create(:user) }
context 'before impersonating' do
subject { visit admin_user_path(user_to_visit) }
@@ -156,7 +156,7 @@ RSpec.describe 'Admin::Users::User', feature_category: :user_management do
it 'disables impersonate button' do
subject
- impersonate_btn = find('[data-testid="impersonate-user-link"]')
+ impersonate_btn = find_by_testid('impersonate-user-link')
expect(impersonate_btn).not_to be_nil
expect(impersonate_btn['disabled']).not_to be_nil
@@ -174,7 +174,7 @@ RSpec.describe 'Admin::Users::User', feature_category: :user_management do
subject
expect(page).to have_content('Impersonate')
- impersonate_btn = find('[data-testid="impersonate-user-link"]')
+ impersonate_btn = find_by_testid('impersonate-user-link')
expect(impersonate_btn['disabled']).to be_nil
end
end
@@ -257,15 +257,13 @@ RSpec.describe 'Admin::Users::User', feature_category: :user_management do
visit admin_user_path(another_user)
end
- it 'logs in as the user when impersonate is clicked' do
+ it 'logs in as the user when impersonate is clicked', :js do
subject
- find('[data-testid="user-dropdown"]').click
-
- expect(page.find(:css, '[data-testid="user-profile-link"]')['data-user']).to eql(another_user.username)
+ expect(page).to have_button("#{another_user.name} user’s menu")
end
- it 'sees impersonation log out icon' do
+ it 'sees impersonation log out icon', :js do
subject
icon = first('[data-testid="incognito-icon"]')
@@ -306,8 +304,8 @@ RSpec.describe 'Admin::Users::User', feature_category: :user_management do
end
end
- context 'ending impersonation' do
- subject { find(:css, 'li.impersonation a').click }
+ context 'ending impersonation', :js do
+ subject { click_on 'Stop impersonating' }
before do
visit admin_user_path(another_user)
@@ -317,9 +315,7 @@ RSpec.describe 'Admin::Users::User', feature_category: :user_management do
it 'logs out of impersonated user back to original user' do
subject
- find('[data-testid="user-dropdown"]').click
-
- expect(page.find(:css, '[data-testid="user-profile-link"]')['data-user']).to eq(current_user.username)
+ expect(page).to have_button("#{current_user.name} user’s menu")
end
it 'is redirected back to the impersonated users page in the admin after stopping' do
diff --git a/spec/features/admin/users/users_spec.rb b/spec/features/admin/users/users_spec.rb
index 8ee30c50a7d..4e988674858 100644
--- a/spec/features/admin/users/users_spec.rb
+++ b/spec/features/admin/users/users_spec.rb
@@ -43,7 +43,7 @@ RSpec.describe 'Admin::Users', feature_category: :user_management do
end
it 'clicking edit user takes us to edit page', :aggregate_failures do
- page.within("[data-testid='user-actions-#{user.id}']") do
+ within_testid("user-actions-#{user.id}") do
click_link 'Edit'
end
@@ -71,7 +71,7 @@ RSpec.describe 'Admin::Users', feature_category: :user_management do
it 'displays count of users projects' do
visit admin_users_path
- expect(page.find("[data-testid='user-project-count-#{current_user.id}']").text).to eq("1")
+ expect(find_by_testid("user-project-count-#{current_user.id}").text).to eq("1")
end
end
@@ -321,7 +321,7 @@ RSpec.describe 'Admin::Users', feature_category: :user_management do
click_user_dropdown_toggle(user.id)
- find('[data-testid="approve"]').click
+ find_by_testid('approve').click
expect(page).to have_content("Approve user #{user.name}?")
@@ -378,7 +378,7 @@ RSpec.describe 'Admin::Users', feature_category: :user_management do
wait_for_requests
- expect(page.find("[data-testid='user-group-count-#{current_user.id}']").text).to eq("2")
+ expect(find_by_testid("user-group-count-#{current_user.id}").text).to eq("2")
end
end
end
@@ -542,7 +542,7 @@ RSpec.describe 'Admin::Users', feature_category: :user_management do
it 'allows group membership to be revoked', :js do
page.within(first('.group_member')) do
- find('.btn[data-testid="remove-user"]').click
+ find_by_testid('remove-user').click
end
accept_gl_confirm(button_text: 'Remove')
@@ -587,7 +587,7 @@ RSpec.describe 'Admin::Users', feature_category: :user_management do
end
def check_breadcrumb(content)
- expect(find('[data-testid="breadcrumb-current-link"]')).to have_content(content)
+ expect(find_by_testid('breadcrumb-current-link')).to have_content(content)
end
end
diff --git a/spec/features/admin_variables_spec.rb b/spec/features/admin_variables_spec.rb
index 91e7a46849c..caa94209e50 100644
--- a/spec/features/admin_variables_spec.rb
+++ b/spec/features/admin_variables_spec.rb
@@ -13,13 +13,12 @@ RSpec.describe 'Instance variables', :js, feature_category: :secrets_management
sign_in(admin)
gitlab_enable_admin_mode_sign_in(admin)
- stub_feature_flags(ci_variable_drawer: false)
visit page_path
wait_for_requests
end
context 'when ci_variables_pages FF is enabled' do
- it_behaves_like 'variable list', is_admin: true
+ it_behaves_like 'variable list drawer', is_admin: true
it_behaves_like 'variable list pagination', :ci_instance_variable
end
@@ -28,16 +27,6 @@ RSpec.describe 'Instance variables', :js, feature_category: :secrets_management
stub_feature_flags(ci_variables_pages: false)
end
- it_behaves_like 'variable list', is_admin: true
- end
-
- context 'when ci_variable_drawer FF is enabled' do
- before do
- stub_feature_flags(ci_variable_drawer: true)
- visit page_path
- wait_for_requests
- end
-
it_behaves_like 'variable list drawer', is_admin: true
end
end
diff --git a/spec/features/alert_management/alert_details_spec.rb b/spec/features/alert_management/alert_details_spec.rb
index 66b7a9ca46c..58ce9e68468 100644
--- a/spec/features/alert_management/alert_details_spec.rb
+++ b/spec/features/alert_management/alert_details_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
RSpec.describe 'Alert details', :js, feature_category: :incident_management do
let_it_be(:project) { create(:project) }
- let_it_be(:developer) { create(:user, :no_super_sidebar) }
+ let_it_be(:developer) { create(:user) }
let_it_be(:alert) { create(:alert_management_alert, project: project, status: 'triggered', title: 'Alert') }
before_all do
@@ -27,7 +27,7 @@ RSpec.describe 'Alert details', :js, feature_category: :incident_management do
it 'shows the alert tabs' do
page.within('.alert-management-details') do
- alert_tabs = find('[data-testid="alertDetailsTabs"]')
+ alert_tabs = find_by_testid('alertDetailsTabs')
expect(alert_tabs).to have_content('Alert details')
expect(alert_tabs).to have_content('Metrics')
@@ -47,10 +47,10 @@ RSpec.describe 'Alert details', :js, feature_category: :incident_management do
it 'updates the alert todo button from the right sidebar' do
expect(page).to have_selector('[data-testid="alert-todo-button"]')
- todo_button = find('[data-testid="alert-todo-button"]')
+ todo_button = find_by_testid('alert-todo-button')
expect(todo_button).to have_content('Add a to do')
- find('[data-testid="alert-todo-button"]').click
+ find_by_testid('alert-todo-button').click
wait_for_requests
expect(todo_button).to have_content('Mark as done')
@@ -58,7 +58,7 @@ RSpec.describe 'Alert details', :js, feature_category: :incident_management do
it 'updates the alert status from the right sidebar' do
page.within('.alert-status') do
- alert_status = find('[data-testid="status"]')
+ alert_status = find_by_testid('status')
expect(alert_status).to have_content('Triggered')
@@ -77,7 +77,7 @@ RSpec.describe 'Alert details', :js, feature_category: :incident_management do
expect(alert_assignee).to have_content('None - assign yourself')
- find('[data-testid="unassigned-users"]').click
+ find_by_testid('unassigned-users').click
wait_for_requests
diff --git a/spec/features/alert_management/alert_management_list_spec.rb b/spec/features/alert_management/alert_management_list_spec.rb
index cc54af249e1..058447b3be3 100644
--- a/spec/features/alert_management/alert_management_list_spec.rb
+++ b/spec/features/alert_management/alert_management_list_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
RSpec.describe 'Alert Management index', :js, feature_category: :incident_management do
let_it_be(:project) { create(:project) }
- let_it_be(:developer) { create(:user, :no_super_sidebar) }
+ let_it_be(:developer) { create(:user) }
before_all do
project.add_developer(developer)
@@ -22,7 +22,7 @@ RSpec.describe 'Alert Management index', :js, feature_category: :incident_manage
expect(page).to have_content('Alerts')
expect(page).to have_content('Surface alerts in GitLab')
expect(page).not_to have_selector('.gl-table')
- page.within('.layout-page') do
+ page.within('.content-wrapper') do
expect(page).not_to have_css('[data-testid="search-icon"]')
end
end
@@ -31,7 +31,7 @@ RSpec.describe 'Alert Management index', :js, feature_category: :incident_manage
it 'renders correctly' do
expect(page).to have_content('Alerts')
expect(page).to have_selector('.gl-table')
- page.within('.layout-page') do
+ page.within('.content-wrapper') do
expect(page).to have_css('[data-testid="search-icon"]')
end
end
diff --git a/spec/features/boards/board_filters_spec.rb b/spec/features/boards/board_filters_spec.rb
index 1ee02de9a66..a6d5d4926ff 100644
--- a/spec/features/boards/board_filters_spec.rb
+++ b/spec/features/boards/board_filters_spec.rb
@@ -15,7 +15,7 @@ RSpec.describe 'Issue board filters', :js, feature_category: :team_planning do
let_it_be(:issue_2) { create(:labeled_issue, project: project, milestone: milestone_2, assignees: [user], labels: [project_label], confidential: true) }
let_it_be(:award_emoji1) { create(:award_emoji, name: 'thumbsup', user: user, awardable: issue_1) }
- let(:filtered_search) { find('[data-testid="issue-board-filtered-search"]') }
+ let(:filtered_search) { find_by_testid('issue-board-filtered-search') }
let(:filter_input) { find('.gl-filtered-search-term-input') }
let(:filter_dropdown) { find('.gl-filtered-search-suggestion-list') }
let(:filter_first_suggestion) { find('.gl-filtered-search-suggestion-list').first('.gl-filtered-search-suggestion') }
@@ -25,7 +25,6 @@ RSpec.describe 'Issue board filters', :js, feature_category: :team_planning do
let_it_be(:board) { create(:board, project: project) }
before do
- stub_feature_flags(apollo_boards: false)
project.add_maintainer(user)
sign_in(user)
diff --git a/spec/features/boards/boards_spec.rb b/spec/features/boards/boards_spec.rb
index 85e54c0f451..48b978f7245 100644
--- a/spec/features/boards/boards_spec.rb
+++ b/spec/features/boards/boards_spec.rb
@@ -28,13 +28,12 @@ RSpec.describe 'Project issue boards', :js, feature_category: :team_planning do
let_it_be(:user, reload: true) { create(:user) }
let_it_be(:user2, reload: true) { create(:user) }
- let(:filtered_search) { find('[data-testid="issue-board-filtered-search"]') }
+ let(:filtered_search) { find_by_testid('issue-board-filtered-search') }
let(:filter_input) { find('.gl-filtered-search-term-input') }
let(:filter_submit) { find('.gl-search-box-by-click-search-button') }
context 'signed in user' do
before do
- stub_feature_flags(apollo_boards: false)
project.add_maintainer(user)
project.add_maintainer(user2)
@@ -296,7 +295,7 @@ RSpec.describe 'Project issue boards', :js, feature_category: :team_planning do
it 'shows issue count on the list' do
page.within(find(".board:nth-child(2)")) do
- expect(page.find('[data-testid="board-items-count"]')).to have_text(total_planning_issues)
+ expect(find_by_testid('board-items-count')).to have_text(total_planning_issues)
expect(page).not_to have_selector('.max-issue-size')
end
end
@@ -389,7 +388,7 @@ RSpec.describe 'Project issue boards', :js, feature_category: :team_planning do
wait_for_board_cards(2, 1)
- find('[data-testid="filtered-search-clear-button"]').click
+ find_by_testid('filtered-search-clear-button').click
filter_submit.click
end
@@ -518,7 +517,6 @@ RSpec.describe 'Project issue boards', :js, feature_category: :team_planning do
context 'signed out user' do
before do
- stub_feature_flags(apollo_boards: false)
visit project_board_path(project, board)
wait_for_requests
end
@@ -540,7 +538,6 @@ RSpec.describe 'Project issue boards', :js, feature_category: :team_planning do
let_it_be(:user_guest, reload: true) { create(:user) }
before do
- stub_feature_flags(apollo_boards: false)
project.add_guest(user_guest)
sign_in(user_guest)
visit project_board_path(project, board)
diff --git a/spec/features/boards/issue_ordering_spec.rb b/spec/features/boards/issue_ordering_spec.rb
index 35e387c9d8a..625a8ddad84 100644
--- a/spec/features/boards/issue_ordering_spec.rb
+++ b/spec/features/boards/issue_ordering_spec.rb
@@ -15,7 +15,6 @@ RSpec.describe 'Issue Boards', :js, feature_category: :team_planning do
let!(:issue3) { create(:labeled_issue, project: project, title: 'testing 3', labels: [label], relative_position: 1) }
before do
- stub_feature_flags(apollo_boards: false)
project.add_maintainer(user)
sign_in(user)
@@ -131,7 +130,7 @@ RSpec.describe 'Issue Boards', :js, feature_category: :team_planning do
end
context 'ordering in list using move to position' do
- let(:move_to_position) { find('[data-testid="board-move-to-position"]') }
+ let(:move_to_position) { find_by_testid('board-move-to-position') }
before do
visit project_board_path(project, board)
diff --git a/spec/features/boards/multiple_boards_spec.rb b/spec/features/boards/multiple_boards_spec.rb
index 9d59d3dd02a..e9d34c6f87f 100644
--- a/spec/features/boards/multiple_boards_spec.rb
+++ b/spec/features/boards/multiple_boards_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe 'Multiple Issue Boards', :js, feature_category: :team_planning do
- let_it_be(:user) { create(:user, :no_super_sidebar) }
+ let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project, :public) }
let_it_be(:planning) { create(:label, project: project, name: 'Planning') }
let_it_be(:board) { create(:board, name: 'board1', project: project) }
diff --git a/spec/features/boards/new_issue_spec.rb b/spec/features/boards/new_issue_spec.rb
index 682ccca38bd..1e44e1d35f9 100644
--- a/spec/features/boards/new_issue_spec.rb
+++ b/spec/features/boards/new_issue_spec.rb
@@ -11,11 +11,7 @@ RSpec.describe 'Issue Boards new issue', :js, feature_category: :team_planning d
let_it_be(:existing_issue) { create(:issue, project: project, title: 'other issue', relative_position: 50) }
let(:board_list_header) { first('[data-testid="board-list-header"]') }
- let(:project_select_dropdown) { find('[data-testid="project-select-dropdown"]') }
-
- before do
- stub_feature_flags(apollo_boards: false)
- end
+ let(:project_select_dropdown) { find_by_testid('project-select-dropdown') }
context 'authorized user' do
before do
@@ -100,7 +96,7 @@ RSpec.describe 'Issue Boards new issue', :js, feature_category: :team_planning d
wait_for_requests
- page.within('[data-testid="sidebar-labels"]') do
+ within_testid('sidebar-labels') do
click_button 'Edit'
wait_for_requests
diff --git a/spec/features/boards/reload_boards_on_browser_back_spec.rb b/spec/features/boards/reload_boards_on_browser_back_spec.rb
index 036daee7655..0ca680c5ed5 100644
--- a/spec/features/boards/reload_boards_on_browser_back_spec.rb
+++ b/spec/features/boards/reload_boards_on_browser_back_spec.rb
@@ -9,8 +9,6 @@ RSpec.describe 'Ensure Boards do not show stale data on browser back', :js, feat
context 'authorized user' do
before do
- stub_feature_flags(apollo_boards: false)
-
project.add_maintainer(user)
sign_in(user)
diff --git a/spec/features/boards/sidebar_assignee_spec.rb b/spec/features/boards/sidebar_assignee_spec.rb
index 899ab5863e1..93e45b3e3f8 100644
--- a/spec/features/boards/sidebar_assignee_spec.rb
+++ b/spec/features/boards/sidebar_assignee_spec.rb
@@ -65,7 +65,7 @@ RSpec.describe 'Project issue boards sidebar assignee', :js,
wait_for_requests
page.within('.dropdown-menu-user') do
- find('[data-testid="unassign"]').click
+ find_by_testid('unassign').click
end
expect(page).to have_content('None')
diff --git a/spec/features/boards/sidebar_labels_in_namespaces_spec.rb b/spec/features/boards/sidebar_labels_in_namespaces_spec.rb
index 68c2b2587e7..da3dd6ba071 100644
--- a/spec/features/boards/sidebar_labels_in_namespaces_spec.rb
+++ b/spec/features/boards/sidebar_labels_in_namespaces_spec.rb
@@ -14,8 +14,6 @@ RSpec.describe 'Issue boards sidebar labels select', :js, feature_category: :tea
let_it_be(:group_board) { create(:board, group: group) }
before do
- stub_feature_flags(apollo_boards: false)
-
load_board group_board_path(group, group_board)
end
diff --git a/spec/features/boards/sidebar_labels_spec.rb b/spec/features/boards/sidebar_labels_spec.rb
index 460d0d232b3..0560cbbfae7 100644
--- a/spec/features/boards/sidebar_labels_spec.rb
+++ b/spec/features/boards/sidebar_labels_spec.rb
@@ -20,7 +20,6 @@ RSpec.describe 'Project issue boards sidebar labels', :js, feature_category: :te
let(:card) { find('.board:nth-child(2)').first('.board-card') }
before do
- stub_feature_flags(apollo_boards: false)
project.add_maintainer(user)
sign_in(user)
diff --git a/spec/features/boards/sidebar_spec.rb b/spec/features/boards/sidebar_spec.rb
index 71cc9a28575..893f1c246a0 100644
--- a/spec/features/boards/sidebar_spec.rb
+++ b/spec/features/boards/sidebar_spec.rb
@@ -15,7 +15,6 @@ RSpec.describe 'Project issue boards sidebar', :js, feature_category: :team_plan
let_it_be(:issue, reload: true) { create(:issue, project: project, relative_position: 1) }
before do
- stub_feature_flags(apollo_boards: false)
project.add_maintainer(user)
sign_in(user)
diff --git a/spec/features/boards/user_adds_lists_to_board_spec.rb b/spec/features/boards/user_adds_lists_to_board_spec.rb
index cc2afca7657..d202c2a1f7d 100644
--- a/spec/features/boards/user_adds_lists_to_board_spec.rb
+++ b/spec/features/boards/user_adds_lists_to_board_spec.rb
@@ -29,7 +29,6 @@ RSpec.describe 'User adds lists', :js, feature_category: :team_planning do
with_them do
before do
- stub_feature_flags(apollo_boards: false)
sign_in(user)
set_cookie('sidebar_collapsed', 'true')
diff --git a/spec/features/boards/user_visits_board_spec.rb b/spec/features/boards/user_visits_board_spec.rb
index 4741f58d883..cf8709b3a76 100644
--- a/spec/features/boards/user_visits_board_spec.rb
+++ b/spec/features/boards/user_visits_board_spec.rb
@@ -44,7 +44,6 @@ RSpec.describe 'User visits issue boards', :js, feature_category: :team_planning
with_them do
before do
- stub_feature_flags(apollo_boards: false)
visit board_path
wait_for_requests
@@ -60,7 +59,6 @@ RSpec.describe 'User visits issue boards', :js, feature_category: :team_planning
end
context "project boards" do
- stub_feature_flags(apollo_boards: false)
let_it_be(:board) { create_default(:board, project: project) }
let(:board_path) { project_boards_path(project, params) }
@@ -69,7 +67,6 @@ RSpec.describe 'User visits issue boards', :js, feature_category: :team_planning
end
context "group boards" do
- stub_feature_flags(apollo_boards: false)
let_it_be(:board) { create_default(:board, group: group) }
let(:board_path) { group_boards_path(group, params) }
diff --git a/spec/features/broadcast_messages_spec.rb b/spec/features/broadcast_messages_spec.rb
index 98f87face15..f887242384c 100644
--- a/spec/features/broadcast_messages_spec.rb
+++ b/spec/features/broadcast_messages_spec.rb
@@ -125,8 +125,8 @@ RSpec.describe 'Broadcast Messages', feature_category: :onboarding do
visit admin_broadcast_messages_path
- page.within('[data-testid="message-row"]', match: :first) do
- find("[data-testid='delete-message-#{message.id}']").click
+ within_testid('message-row', match: :first) do
+ find_by_testid("delete-message-#{message.id}").click
end
accept_gl_confirm(button_text: 'Delete message')
@@ -145,7 +145,7 @@ RSpec.describe 'Broadcast Messages', feature_category: :onboarding do
end
def expect_broadcast_message(text)
- page.within('[data-testid="banner-broadcast-message"]') do
+ within_testid('banner-broadcast-message') do
expect(page).to have_content text
end
end
@@ -157,7 +157,7 @@ RSpec.describe 'Broadcast Messages', feature_category: :onboarding do
end
def expect_to_be_on_explore_projects_page
- page.within('[data-testid="explore-projects-title"]') do
+ within_testid('explore-projects-title') do
expect(page).to have_content 'Explore projects'
end
end
diff --git a/spec/features/calendar_spec.rb b/spec/features/calendar_spec.rb
index e22ae4f51fb..291c40f0f6b 100644
--- a/spec/features/calendar_spec.rb
+++ b/spec/features/calendar_spec.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe 'Contributions Calendar', :js, feature_category: :user_profile do
include MobileHelpers
- let(:user) { create(:user, :no_super_sidebar) }
+ let(:user) { create(:user) }
let(:contributed_project) { create(:project, :public, :repository) }
let(:issue_note) { create(:note, project: contributed_project) }
@@ -83,7 +83,6 @@ RSpec.describe 'Contributions Calendar', :js, feature_category: :user_profile do
shared_context 'when user page is visited' do
before do
visit user.username
- page.click_link('Overview')
wait_for_requests
end
end
diff --git a/spec/features/callouts/registration_enabled_spec.rb b/spec/features/callouts/registration_enabled_spec.rb
index 3282a40854d..4cd5cb9a857 100644
--- a/spec/features/callouts/registration_enabled_spec.rb
+++ b/spec/features/callouts/registration_enabled_spec.rb
@@ -43,7 +43,7 @@ RSpec.describe 'Registration enabled callout', feature_category: :system_access
before do
visit admin_root_path
- find('[data-testid="close-registration-enabled-callout"]').click
+ find_by_testid('close-registration-enabled-callout').click
wait_for_requests
diff --git a/spec/features/clusters/create_agent_spec.rb b/spec/features/clusters/create_agent_spec.rb
index d90c43f452c..79eaecdf582 100644
--- a/spec/features/clusters/create_agent_spec.rb
+++ b/spec/features/clusters/create_agent_spec.rb
@@ -27,7 +27,7 @@ RSpec.describe 'Cluster agent registration', :js, feature_category: :deployment_
end
it 'allows the user to select an agent to install, and displays the resulting agent token' do
- find('[data-testid="clusters-default-action-button"]').click
+ find_by_testid('clusters-default-action-button').click
expect(page).to have_content('Register')
diff --git a/spec/features/commit_spec.rb b/spec/features/commit_spec.rb
index 61792ea5a58..739a070f423 100644
--- a/spec/features/commit_spec.rb
+++ b/spec/features/commit_spec.rb
@@ -66,12 +66,4 @@ RSpec.describe 'Commit', feature_category: :source_code_management do
end
it_behaves_like "single commit view"
-
- context "when super sidebar is enabled" do
- before do
- user.update!(use_new_navigation: true)
- end
-
- it_behaves_like "single commit view"
- end
end
diff --git a/spec/features/commits_spec.rb b/spec/features/commits_spec.rb
index 5f880af37dc..8f6c1c28872 100644
--- a/spec/features/commits_spec.rb
+++ b/spec/features/commits_spec.rb
@@ -41,13 +41,13 @@ RSpec.describe 'Commits', feature_category: :source_code_management do
end
it 'contains commit short id' do
- page.within('[data-testid="pipeline-details-header"]') do
+ within_testid('pipeline-details-header') do
expect(page).to have_content pipeline.sha[0..7]
end
end
it 'contains generic commit status build' do
- page.within('[data-testid="jobs-tab-table"]') do
+ within_testid('jobs-tab-table') do
expect(page).to have_content "##{status.id}" # build id
expect(page).to have_content 'generic' # build name
end
@@ -81,8 +81,8 @@ RSpec.describe 'Commits', feature_category: :source_code_management do
it 'shows correct build status from default branch' do
page.within("//li[@id='commit-#{pipeline.short_sha}']") do
- expect(page).to have_css("[data-testid='ci-status-badge-legacy']")
- expect(page).to have_css('.ci-status-icon-success')
+ expect(page).to have_css("[data-testid='ci-icon']")
+ expect(page).to have_css('[data-testid="status_success_borderless-icon"]')
end
end
end
@@ -122,7 +122,7 @@ RSpec.describe 'Commits', feature_category: :source_code_management do
describe 'Cancel build' do
it 'cancels build', :js, :sidekiq_might_not_need_inline do
visit pipeline_path(pipeline)
- find('[data-testid="cancel-pipeline"]').click
+ find_by_testid('cancel-pipeline').click
expect(page).to have_content 'Canceled'
end
end
diff --git a/spec/features/contextual_sidebar_spec.rb b/spec/features/contextual_sidebar_spec.rb
index ab322f18240..dffc87c2028 100644
--- a/spec/features/contextual_sidebar_spec.rb
+++ b/spec/features/contextual_sidebar_spec.rb
@@ -4,39 +4,19 @@ require 'spec_helper'
RSpec.describe 'Contextual sidebar', :js, feature_category: :remote_development do
context 'when context is a project' do
- let_it_be(:user) { create(:user, :no_super_sidebar) }
+ let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project, :repository, namespace: user.namespace) }
before do
sign_in(user)
+ visit project_path(project)
end
- context 'when analyzing the menu' do
- before do
- visit project_path(project)
- end
+ it 'shows flyout menu on other section on hover' do
+ expect(page).not_to have_link('Pipelines', href: project_pipelines_path(project))
- it 'shows flyout navs when collapsed or expanded apart from on the active item when expanded', :aggregate_failures do
- expect(page).not_to have_selector('.js-sidebar-collapsed')
-
- find('.rspec-link-pipelines').hover
-
- expect(page).to have_selector('.is-showing-fly-out')
-
- find('.rspec-project-link').hover
-
- expect(page).not_to have_selector('.is-showing-fly-out')
-
- find('.rspec-toggle-sidebar').click
-
- find('.rspec-link-pipelines').hover
-
- expect(page).to have_selector('.is-showing-fly-out')
-
- find('.rspec-project-link').hover
-
- expect(page).to have_selector('.is-showing-fly-out')
- end
+ find_button('Build').hover
+ expect(page).to have_link('Pipelines', href: project_pipelines_path(project))
end
end
end
diff --git a/spec/features/dashboard/activity_spec.rb b/spec/features/dashboard/activity_spec.rb
index 61631d28aa9..fd1e5a34f05 100644
--- a/spec/features/dashboard/activity_spec.rb
+++ b/spec/features/dashboard/activity_spec.rb
@@ -2,8 +2,8 @@
require 'spec_helper'
-RSpec.describe 'Dashboard > Activity', feature_category: :user_profile do
- let(:user) { create(:user, :no_super_sidebar) }
+RSpec.describe 'Dashboard > Activity', :js, feature_category: :user_profile do
+ let(:user) { create(:user) }
before do
sign_in(user)
@@ -46,7 +46,7 @@ RSpec.describe 'Dashboard > Activity', feature_category: :user_profile do
it_behaves_like "an autodiscoverable RSS feed with current_user's feed token"
end
- context 'event filters', :js do
+ context 'event filters' do
let(:project) { create(:project, :repository) }
let(:merge_request) do
diff --git a/spec/features/dashboard/group_dashboard_with_external_authorization_service_spec.rb b/spec/features/dashboard/group_dashboard_with_external_authorization_service_spec.rb
deleted file mode 100644
index a00666c2376..00000000000
--- a/spec/features/dashboard/group_dashboard_with_external_authorization_service_spec.rb
+++ /dev/null
@@ -1,43 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe 'The group dashboard', :js, feature_category: :groups_and_projects do
- include ExternalAuthorizationServiceHelpers
- include Features::TopNavSpecHelpers
-
- let(:user) { create(:user, :no_super_sidebar) }
-
- before do
- sign_in user
- end
-
- describe 'The top navigation' do
- it 'has all the expected links' do
- visit dashboard_groups_path
-
- open_top_nav
-
- within_top_nav do
- expect(page).to have_button('Projects')
- expect(page).to have_button('Groups')
- expect(page).to have_link('Your work')
- expect(page).to have_link('Explore')
- end
- end
-
- it 'hides some links when an external authorization service is enabled' do
- enable_external_authorization_service_check
- visit dashboard_groups_path
-
- open_top_nav
-
- within_top_nav do
- expect(page).to have_button('Projects')
- expect(page).to have_button('Groups')
- expect(page).to have_link('Your work')
- expect(page).to have_link('Explore')
- end
- end
- end
-end
diff --git a/spec/features/dashboard/group_spec.rb b/spec/features/dashboard/group_spec.rb
index ea600758607..7510a92e19b 100644
--- a/spec/features/dashboard/group_spec.rb
+++ b/spec/features/dashboard/group_spec.rb
@@ -15,7 +15,7 @@ RSpec.describe 'Dashboard Group', feature_category: :groups_and_projects do
it 'creates new group', :js do
visit dashboard_groups_path
- find('[data-testid="new-group-button"]').click
+ find_by_testid('new-group-button').click
click_link 'Create group'
new_name = 'Samurai'
diff --git a/spec/features/dashboard/groups_list_spec.rb b/spec/features/dashboard/groups_list_spec.rb
index e1da163cdf5..745e45478d1 100644
--- a/spec/features/dashboard/groups_list_spec.rb
+++ b/spec/features/dashboard/groups_list_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe 'Dashboard Groups page', :js, feature_category: :groups_and_projects do
- let(:user) { create(:user, :no_super_sidebar) }
+ let(:user) { create(:user) }
let(:group) { create(:group) }
let(:nested_group) { create(:group, :nested) }
let(:another_group) { create(:group) }
diff --git a/spec/features/dashboard/issuables_counter_spec.rb b/spec/features/dashboard/issuables_counter_spec.rb
index 501405c5662..d34f8cb3e18 100644
--- a/spec/features/dashboard/issuables_counter_spec.rb
+++ b/spec/features/dashboard/issuables_counter_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe 'Navigation bar counter', :use_clean_rails_memory_store_caching, feature_category: :team_planning do
- let(:user) { create(:user, :no_super_sidebar) }
+ let(:user) { create(:user) }
let(:project) { create(:project, namespace: user.namespace) }
let(:issue) { create(:issue, project: project) }
let(:merge_request) { create(:merge_request, source_project: project) }
@@ -17,33 +17,29 @@ RSpec.describe 'Navigation bar counter', :use_clean_rails_memory_store_caching,
it 'reflects dashboard issues count', :js do
visit issues_path
- expect_counters('issues', '1', n_("%d assigned issue", "%d assigned issues", 1) % 1)
+ expect_issue_count(1)
issue.update!(assignees: [])
- Users::AssignedIssuesCountService.new(current_user: user).delete_cache
+ user.invalidate_cache_counts
- travel_to(3.minutes.from_now) do
- visit issues_path
+ visit issues_path
- expect_counters('issues', '0', n_("%d assigned issue", "%d assigned issues", 0) % 0)
- end
+ expect_issue_count(0)
end
it 'reflects dashboard merge requests count', :js do
visit merge_requests_path
- expect_counters('merge_requests', '1', n_("%d merge request", "%d merge requests", 1) % 1)
+ expect_merge_request_count(1)
merge_request.update!(assignees: [])
user.invalidate_cache_counts
- travel_to(3.minutes.from_now) do
- visit merge_requests_path
+ visit merge_requests_path
- expect_counters('merge_requests', '0', n_("%d merge request", "%d merge requests", 0) % 0)
- end
+ expect_merge_request_count(0)
end
def issues_path
@@ -54,11 +50,21 @@ RSpec.describe 'Navigation bar counter', :use_clean_rails_memory_store_caching,
merge_requests_dashboard_path(assignee_username: user.username)
end
- def expect_counters(issuable_type, count, badge_label)
+ def expect_issue_count(count)
dashboard_count = find('.gl-tabs-nav li a.active')
+ expect(dashboard_count).to have_content(count)
+ within_testid('super-sidebar') do
+ expect(page).to have_link("Issues #{count}")
+ end
+ end
+
+ def expect_merge_request_count(count)
+ dashboard_count = find('.gl-tabs-nav li a.active')
expect(dashboard_count).to have_content(count)
- expect(page).to have_css(".dashboard-shortcuts-#{issuable_type}", visible: :all, text: count)
- expect(page).to have_css("span[aria-label='#{badge_label}']", visible: :all, text: count)
+
+ within_testid('super-sidebar') do
+ expect(page).to have_button("Merge requests #{count}")
+ end
end
end
diff --git a/spec/features/dashboard/issues_spec.rb b/spec/features/dashboard/issues_spec.rb
index 69b32113bba..7008f702622 100644
--- a/spec/features/dashboard/issues_spec.rb
+++ b/spec/features/dashboard/issues_spec.rb
@@ -2,10 +2,10 @@
require 'spec_helper'
-RSpec.describe 'Dashboard Issues', feature_category: :team_planning do
+RSpec.describe 'Dashboard Issues', :js, feature_category: :team_planning do
include FilteredSearchHelpers
- let_it_be(:current_user) { create(:user, :no_super_sidebar) }
+ let_it_be(:current_user) { create(:user) }
let_it_be(:user) { current_user } # Shared examples depend on this being available
let_it_be(:public_project) { create(:project, :public) }
let_it_be(:project) { create(:project) }
@@ -23,7 +23,7 @@ RSpec.describe 'Dashboard Issues', feature_category: :team_planning do
it_behaves_like 'a "Your work" page with sidebar and breadcrumbs', :issues_dashboard_path, :issues
- describe 'issues', :js do
+ describe 'issues' do
it 'shows issues assigned to current user' do
expect(page).to have_content(assigned_issue.title)
expect(page).not_to have_content(authored_issue.title)
@@ -59,23 +59,25 @@ RSpec.describe 'Dashboard Issues', feature_category: :team_planning do
end
describe 'new issue dropdown' do
- it 'shows projects only with issues feature enabled', :js do
+ it 'shows projects only with issues feature enabled' do
click_button _('Select project to create issue')
- page.within('[data-testid="new-resource-dropdown"] [role="menu"]') do
- expect(page).to have_content(project.full_name)
- expect(page).not_to have_content(project_with_issues_disabled.full_name)
+ within_testid('new-resource-dropdown') do
+ within('[role="menu"]') do
+ expect(page).to have_content(project.full_name)
+ expect(page).not_to have_content(project_with_issues_disabled.full_name)
+ end
end
end
- it 'shows the new issue page', :js do
+ it 'shows the new issue page' do
click_button _('Select project to create issue')
wait_for_requests
project_path = "/#{project.full_path}"
- page.within('[data-testid="new-resource-dropdown"]') do
+ within_testid('new-resource-dropdown') do
find_button(project.full_name).click
end
diff --git a/spec/features/dashboard/merge_requests_spec.rb b/spec/features/dashboard/merge_requests_spec.rb
index 4bb04f4ff80..8a7652858b8 100644
--- a/spec/features/dashboard/merge_requests_spec.rb
+++ b/spec/features/dashboard/merge_requests_spec.rb
@@ -2,12 +2,12 @@
require 'spec_helper'
-RSpec.describe 'Dashboard Merge Requests', feature_category: :code_review_workflow do
+RSpec.describe 'Dashboard Merge Requests', :js, feature_category: :code_review_workflow do
include Features::SortingHelpers
include FilteredSearchHelpers
include ProjectForksHelper
- let(:current_user) { create(:user, :no_super_sidebar) }
+ let(:current_user) { create(:user) }
let(:user) { current_user }
let(:project) { create(:project) }
@@ -19,7 +19,21 @@ RSpec.describe 'Dashboard Merge Requests', feature_category: :code_review_workfl
sign_in(current_user)
end
- it_behaves_like 'a "Your work" page with sidebar and breadcrumbs', :merge_requests_dashboard_path, :merge_requests
+ describe 'sidebar' do
+ it 'has nav items for assigned MRs and review requests' do
+ visit merge_requests_dashboard_path(assignee_username: user)
+
+ within('#super-sidebar') do
+ expect(page).to have_css("a[data-track-label='merge_requests_assigned'][aria-current='page']")
+ end
+
+ click_link 'Review requests'
+
+ within('#super-sidebar') do
+ expect(page).to have_css("a[data-track-label='merge_requests_to_review'][aria-current='page']")
+ end
+ end
+ end
it 'disables target branch filter' do
visit merge_requests_dashboard_path
@@ -35,11 +49,11 @@ RSpec.describe 'Dashboard Merge Requests', feature_category: :code_review_workfl
visit merge_requests_dashboard_path
end
- it 'shows projects only with merge requests feature enabled', :js do
+ it 'shows projects only with merge requests feature enabled' do
click_button 'Select project to create merge request'
wait_for_requests
- page.within('[data-testid="new-resource-dropdown"]') do
+ within_testid('new-resource-dropdown') do
expect(page).to have_content(project.full_name)
expect(page).not_to have_content(project_with_disabled_merge_requests.full_name)
@@ -132,14 +146,10 @@ RSpec.describe 'Dashboard Merge Requests', feature_category: :code_review_workfl
end
it 'includes assigned and reviewers in badge' do
- within("span[aria-label='#{n_("%d merge request", "%d merge requests", 3) % 3}']") do
- expect(page).to have_content('3')
+ within('#merge-requests') do
+ expect(page).to have_css("a", text: 'Assigned 2')
+ expect(page).to have_css("a", text: 'Review requests 1')
end
-
- find('.dashboard-shortcuts-merge_requests').click
-
- expect(find('.js-assigned-mr-count')).to have_content('2')
- expect(find('.js-reviewer-mr-count')).to have_content('1')
end
it 'shows assigned merge requests' do
@@ -156,7 +166,7 @@ RSpec.describe 'Dashboard Merge Requests', feature_category: :code_review_workfl
expect(page).not_to have_content(review_requested_merge_request.title)
end
- it 'shows authored merge requests', :js do
+ it 'shows authored merge requests' do
reset_filters
input_filtered_search("author:=#{current_user.to_reference}")
@@ -169,7 +179,7 @@ RSpec.describe 'Dashboard Merge Requests', feature_category: :code_review_workfl
expect(page).not_to have_content(other_merge_request.title)
end
- it 'shows labeled merge requests', :js do
+ it 'shows labeled merge requests' do
reset_filters
input_filtered_search("label:=#{label.name}")
@@ -182,13 +192,13 @@ RSpec.describe 'Dashboard Merge Requests', feature_category: :code_review_workfl
expect(page).not_to have_content(other_merge_request.title)
end
- it 'shows error message without filter', :js do
+ it 'shows error message without filter' do
reset_filters
expect(page).to have_content('Please select at least one filter to see results')
end
- it 'shows sorted merge requests', :js do
+ it 'shows sorted merge requests' do
pajamas_sort_by(s_('SortOptions|Created date'))
visit merge_requests_dashboard_path(assignee_username: current_user.username)
@@ -196,7 +206,7 @@ RSpec.describe 'Dashboard Merge Requests', feature_category: :code_review_workfl
expect(find('.issues-filters')).to have_content('Created date')
end
- it 'keeps sorting merge requests after visiting Projects MR page', :js do
+ it 'keeps sorting merge requests after visiting Projects MR page' do
pajamas_sort_by(s_('SortOptions|Created date'))
visit project_merge_requests_path(project)
@@ -205,7 +215,7 @@ RSpec.describe 'Dashboard Merge Requests', feature_category: :code_review_workfl
end
end
- context 'merge request review', :js do
+ context 'merge request review' do
let_it_be(:author_user) { create(:user) }
let!(:review_requested_merge_request) do
diff --git a/spec/features/dashboard/milestones_spec.rb b/spec/features/dashboard/milestones_spec.rb
index 38637115246..915626de33c 100644
--- a/spec/features/dashboard/milestones_spec.rb
+++ b/spec/features/dashboard/milestones_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Dashboard > Milestones', feature_category: :team_planning do
+RSpec.describe 'Dashboard > Milestones', :js, feature_category: :team_planning do
describe 'as anonymous user' do
before do
visit dashboard_milestones_path
@@ -14,7 +14,7 @@ RSpec.describe 'Dashboard > Milestones', feature_category: :team_planning do
end
describe 'as logged-in user' do
- let(:user) { create(:user, :no_super_sidebar) }
+ let(:user) { create(:user) }
let(:group) { create(:group) }
let(:project) { create(:project, namespace: user.namespace) }
let!(:milestone) { create(:milestone, project: project) }
@@ -35,11 +35,11 @@ RSpec.describe 'Dashboard > Milestones', feature_category: :team_planning do
expect(first('.milestone')).to have_content('Merge requests')
end
- describe 'new milestones dropdown', :js do
- it 'takes user to a new milestone page', :js do
+ describe 'new milestones dropdown' do
+ it 'takes user to a new milestone page' do
click_button 'Select project to create milestone'
- page.within('[data-testid="new-resource-dropdown"]') do
+ within_testid('new-resource-dropdown') do
click_button group.name
click_link "New milestone in #{group.name}"
end
@@ -50,7 +50,7 @@ RSpec.describe 'Dashboard > Milestones', feature_category: :team_planning do
end
describe 'with merge requests disabled' do
- let(:user) { create(:user, :no_super_sidebar) }
+ let(:user) { create(:user) }
let(:group) { create(:group) }
let(:project) { create(:project, :merge_requests_disabled, namespace: user.namespace) }
let!(:milestone) { create(:milestone, project: project) }
diff --git a/spec/features/dashboard/navbar_spec.rb b/spec/features/dashboard/navbar_spec.rb
index 30e7f2d2e4e..2ce9eba309d 100644
--- a/spec/features/dashboard/navbar_spec.rb
+++ b/spec/features/dashboard/navbar_spec.rb
@@ -2,10 +2,10 @@
require 'spec_helper'
-RSpec.describe '"Your work" navbar', feature_category: :navigation do
+RSpec.describe '"Your work" navbar', :js, feature_category: :navigation do
include_context 'dashboard navbar structure'
- let_it_be(:user) { create(:user, :no_super_sidebar) }
+ let_it_be(:user) { create(:user) }
it_behaves_like 'verified navigation bar' do
before do
diff --git a/spec/features/dashboard/projects_spec.rb b/spec/features/dashboard/projects_spec.rb
index 90ad6fcea25..5379dabc713 100644
--- a/spec/features/dashboard/projects_spec.rb
+++ b/spec/features/dashboard/projects_spec.rb
@@ -2,8 +2,8 @@
require 'spec_helper'
-RSpec.describe 'Dashboard Projects', feature_category: :groups_and_projects do
- let_it_be(:user) { create(:user, :no_super_sidebar) }
+RSpec.describe 'Dashboard Projects', :js, feature_category: :groups_and_projects do
+ let_it_be(:user) { create(:user) }
let_it_be(:project, reload: true) { create(:project, :repository, creator: build(:user)) } # ensure creator != owner to avoid N+1 false-positive
let_it_be(:project2) { create(:project, :public) }
@@ -91,7 +91,7 @@ RSpec.describe 'Dashboard Projects', feature_category: :groups_and_projects do
expect(find('.gl-tabs-nav li:nth-child(1) .badge-pill')).to have_content(1)
end
- it 'shows personal projects on personal projects tab', :js do
+ it 'shows personal projects on personal projects tab' do
project3 = create(:project, namespace: user.namespace)
visit dashboard_projects_path
@@ -111,7 +111,7 @@ RSpec.describe 'Dashboard Projects', feature_category: :groups_and_projects do
end
end
- context 'when on Starred projects tab', :js do
+ context 'when on Starred projects tab' do
it_behaves_like 'a "Your work" page with sidebar and breadcrumbs', :starred_dashboard_projects_path, :projects
it 'shows the empty state when there are no starred projects' do
@@ -153,8 +153,8 @@ RSpec.describe 'Dashboard Projects', feature_category: :groups_and_projects do
page.within('[data-testid="project_controls"]') do
expect(page).to have_xpath("//a[@href='#{pipelines_project_commit_path(project, project.commit, ref: pipeline.ref)}']")
- expect(page).to have_css("[data-testid='ci-status-badge']")
- expect(page).to have_css('.ci-status-icon-success')
+ expect(page).to have_css("[data-testid='ci-icon']")
+ expect(page).to have_css('[data-testid="status_success_borderless-icon"]')
expect(page).to have_link('Pipeline: passed')
end
end
@@ -165,8 +165,8 @@ RSpec.describe 'Dashboard Projects', feature_category: :groups_and_projects do
page.within('[data-testid="project_controls"]') do
expect(page).not_to have_xpath("//a[@href='#{pipelines_project_commit_path(project, project.commit, ref: pipeline.ref)}']")
- expect(page).not_to have_css("[data-testid='ci-status-badge']")
- expect(page).not_to have_css('.ci-status-icon-success')
+ expect(page).not_to have_css("[data-testid='ci-icon']")
+ expect(page).not_to have_css('[data-testid="status_success_borderless-icon"]')
expect(page).not_to have_link('Pipeline: passed')
end
end
diff --git a/spec/features/dashboard/shortcuts_spec.rb b/spec/features/dashboard/shortcuts_spec.rb
index c8013d364e3..976dcc5a027 100644
--- a/spec/features/dashboard/shortcuts_spec.rb
+++ b/spec/features/dashboard/shortcuts_spec.rb
@@ -50,7 +50,6 @@ RSpec.describe 'Dashboard shortcuts', :js, feature_category: :shared do
context 'logged out' do
before do
- stub_feature_flags(super_sidebar_logged_out: false)
visit explore_root_path
end
diff --git a/spec/features/dashboard/snippets_spec.rb b/spec/features/dashboard/snippets_spec.rb
index f9284f9479e..5ab5a27171c 100644
--- a/spec/features/dashboard/snippets_spec.rb
+++ b/spec/features/dashboard/snippets_spec.rb
@@ -2,8 +2,8 @@
require 'spec_helper'
-RSpec.describe 'Dashboard snippets', feature_category: :source_code_management do
- let_it_be(:user) { create(:user, :no_super_sidebar) }
+RSpec.describe 'Dashboard snippets', :js, feature_category: :source_code_management do
+ let_it_be(:user) { create(:user) }
it_behaves_like 'a "Your work" page with sidebar and breadcrumbs', :dashboard_snippets_path, :snippets
@@ -32,7 +32,7 @@ RSpec.describe 'Dashboard snippets', feature_category: :source_code_management d
end
end
- context 'when there are no project snippets', :js do
+ context 'when there are no project snippets' do
let(:project) { create(:project, :public, creator: user) }
before do
@@ -55,7 +55,7 @@ RSpec.describe 'Dashboard snippets', feature_category: :source_code_management d
it 'shows documentation button in main comment area' do
parent_element = page.find('.row.empty-state')
- expect(parent_element).to have_link('Documentation', href: help_page_path('user/snippets.md'))
+ expect(parent_element).to have_link('Documentation', href: help_page_path('user/snippets'))
end
end
diff --git a/spec/features/dashboard/todos/todos_spec.rb b/spec/features/dashboard/todos/todos_spec.rb
index ade7da0cb49..59ce873905a 100644
--- a/spec/features/dashboard/todos/todos_spec.rb
+++ b/spec/features/dashboard/todos/todos_spec.rb
@@ -2,13 +2,13 @@
require 'spec_helper'
-RSpec.describe 'Dashboard Todos', feature_category: :team_planning do
+RSpec.describe 'Dashboard Todos', :js, feature_category: :team_planning do
include DesignManagementTestHelpers
- let_it_be(:user) { create(:user, :no_super_sidebar, username: 'john') }
- let_it_be(:user2) { create(:user, :no_super_sidebar, username: 'diane') }
+ let_it_be(:user) { create(:user, username: 'john') }
+ let_it_be(:user2) { create(:user, username: 'diane') }
let_it_be(:user3) { create(:user) }
- let_it_be(:author) { create(:user, :no_super_sidebar) }
+ 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") }
@@ -73,7 +73,7 @@ RSpec.describe 'Dashboard Todos', feature_category: :team_planning do
end
end
- context 'User has a todo', :js do
+ context 'User has a todo' do
let_it_be(:user_todo) { create(:todo, :mentioned, user: user, project: project, target: issue, author: author) }
before do
@@ -287,7 +287,7 @@ RSpec.describe 'Dashboard Todos', feature_category: :team_planning do
end
end
- context 'User has done todos', :js do
+ context 'User has done todos' do
before do
create(:todo, :mentioned, :done, user: user, project: project, target: issue, author: author)
sign_in(user)
@@ -359,7 +359,7 @@ RSpec.describe 'Dashboard Todos', feature_category: :team_planning do
expect(page).to have_selector('.gl-pagination .js-pagination-page', count: 2)
end
- describe 'mark all as done', :js do
+ describe 'mark all as done' do
before do
visit dashboard_todos_path
find('.js-todos-mark-all').click
@@ -377,7 +377,7 @@ RSpec.describe 'Dashboard Todos', feature_category: :team_planning do
end
end
- describe 'undo mark all as done', :js do
+ describe 'undo mark all as done' do
before do
visit dashboard_todos_path
end
diff --git a/spec/features/explore/catalog_spec.rb b/spec/features/explore/catalog_spec.rb
new file mode 100644
index 00000000000..52ce52e43fe
--- /dev/null
+++ b/spec/features/explore/catalog_spec.rb
@@ -0,0 +1,80 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Global Catalog', :js, feature_category: :pipeline_composition do
+ let_it_be(:namespace) { create(:group) }
+ let_it_be(:user) { create(:user) }
+
+ before_all do
+ namespace.add_developer(user)
+ end
+
+ before do
+ sign_in(user)
+ end
+
+ describe 'GET explore/catalog' do
+ let_it_be(:project) { create(:project, :repository, namespace: namespace) }
+ let_it_be(:ci_resource_projects) do
+ create_list(
+ :project,
+ 3,
+ :repository,
+ description: 'A simple component',
+ namespace: namespace
+ )
+ end
+
+ before do
+ ci_resource_projects.each do |current_project|
+ create(:ci_catalog_resource, project: current_project)
+ end
+
+ visit explore_catalog_index_path
+ wait_for_requests
+ end
+
+ it 'shows CI Catalog title and description', :aggregate_failures do
+ expect(page).to have_content('CI/CD Catalog')
+ expect(page).to have_content('Discover CI configuration resources for a seamless CI/CD experience.')
+ end
+
+ it 'renders CI Catalog resources list' do
+ expect(find_all('[data-testid="catalog-resource-item"]').length).to be(3)
+ end
+
+ context 'for a single CI/CD catalog resource' do
+ it 'renders resource details', :aggregate_failures do
+ within_testid('catalog-resource-item', match: :first) do
+ expect(page).to have_content(ci_resource_projects[2].name)
+ expect(page).to have_content(ci_resource_projects[2].description)
+ expect(page).to have_content(namespace.name)
+ end
+ end
+
+ context 'when clicked' do
+ before do
+ find_by_testid('ci-resource-link', match: :first).click
+ end
+
+ it 'navigate to the details page' do
+ expect(page).to have_content('Go to the project')
+ end
+ end
+ end
+ end
+
+ describe 'GET explore/catalog/:id' do
+ let_it_be(:project) { create(:project, :repository, namespace: namespace) }
+ let_it_be(:new_ci_resource) { create(:ci_catalog_resource, project: project) }
+
+ before do
+ visit explore_catalog_path(id: new_ci_resource["id"])
+ end
+
+ it 'navigates to the details page' do
+ expect(page).to have_content('Go to the project')
+ end
+ end
+end
diff --git a/spec/features/explore/navbar_spec.rb b/spec/features/explore/navbar_spec.rb
index 853d66ed4d1..c172760eb2c 100644
--- a/spec/features/explore/navbar_spec.rb
+++ b/spec/features/explore/navbar_spec.rb
@@ -2,13 +2,24 @@
require 'spec_helper'
-RSpec.describe '"Explore" navbar', feature_category: :navigation do
+RSpec.describe '"Explore" navbar', :js, feature_category: :navigation do
include_context '"Explore" navbar structure'
it_behaves_like 'verified navigation bar' do
before do
- stub_feature_flags(super_sidebar_logged_out: false)
+ stub_feature_flags(global_ci_catalog: false)
visit explore_projects_path
end
end
+
+ context "with 'global_ci_catalog' enabled" do
+ include_context '"Explore" navbar structure with global_ci_catalog FF'
+
+ it_behaves_like 'verified navigation bar', global_ci_catalog: true do
+ before do
+ stub_feature_flags(global_ci_catalog: true)
+ visit explore_projects_path
+ end
+ end
+ end
end
diff --git a/spec/features/explore/user_explores_projects_spec.rb b/spec/features/explore/user_explores_projects_spec.rb
index 43d464e0c9f..e1341824bfd 100644
--- a/spec/features/explore/user_explores_projects_spec.rb
+++ b/spec/features/explore/user_explores_projects_spec.rb
@@ -3,20 +3,45 @@
require 'spec_helper'
RSpec.describe 'User explores projects', feature_category: :user_profile do
- before do
- stub_feature_flags(super_sidebar_logged_out: false)
+ shared_examples 'an "Explore > Projects" page with sidebar and breadcrumbs' do |page_path|
+ before do
+ visit send(page_path)
+ end
+
+ describe "sidebar", :js do
+ it 'shows the "Explore" sidebar' do
+ has_testid?('super-sidebar')
+ within_testid('super-sidebar') do
+ expect(page).to have_css('#super-sidebar-context-header', text: 'Explore')
+ end
+ end
+
+ it 'shows the "Projects" menu item as active' do
+ within_testid('super-sidebar') do
+ expect(page).to have_css("[aria-current='page']", text: "Projects")
+ end
+ end
+ end
+
+ describe 'breadcrumbs' do
+ it 'has "Explore" as its root breadcrumb' do
+ within '.breadcrumbs-list li:first' do
+ expect(page).to have_link('Explore', href: explore_root_path)
+ end
+ end
+ end
end
describe '"All" tab' do
- it_behaves_like 'an "Explore" page with sidebar and breadcrumbs', :explore_projects_path, :projects
+ it_behaves_like 'an "Explore > Projects" page with sidebar and breadcrumbs', :explore_projects_path
end
describe '"Most starred" tab' do
- it_behaves_like 'an "Explore" page with sidebar and breadcrumbs', :starred_explore_projects_path, :projects
+ it_behaves_like 'an "Explore > Projects" page with sidebar and breadcrumbs', :starred_explore_projects_path
end
describe '"Trending" tab' do
- it_behaves_like 'an "Explore" page with sidebar and breadcrumbs', :trending_explore_projects_path, :projects
+ it_behaves_like 'an "Explore > Projects" page with sidebar and breadcrumbs', :trending_explore_projects_path
end
context 'when some projects exist' do
diff --git a/spec/features/global_search_spec.rb b/spec/features/global_search_spec.rb
index dfafacf48e2..7d6d1648ff5 100644
--- a/spec/features/global_search_spec.rb
+++ b/spec/features/global_search_spec.rb
@@ -3,9 +3,7 @@
require 'spec_helper'
RSpec.describe 'Global search', :js, feature_category: :global_search do
- include AfterNextHelpers
-
- let_it_be(:user) { create(:user, :no_super_sidebar) }
+ let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project, namespace: user.namespace) }
before do
@@ -18,17 +16,18 @@ RSpec.describe 'Global search', :js, feature_category: :global_search do
visit dashboard_projects_path
end
- it 'renders updated search bar' do
- expect(page).to have_no_selector('.search-form')
- expect(page).to have_selector('#js-header-search')
+ it 'renders search button' do
+ expect(page).to have_button('Search or go to…')
end
- it 'focuses search input when shortcut "s" is pressed' do
- expect(page).not_to have_selector('#search:focus')
+ it 'opens search modal when shortcut "s" is pressed' do
+ search_selector = 'input[type="search"]:focus'
+
+ expect(page).not_to have_selector(search_selector)
find('body').native.send_key('s')
- expect(page).to have_selector('#search:focus')
+ expect(page).to have_selector(search_selector)
end
end
end
diff --git a/spec/features/group_variables_spec.rb b/spec/features/group_variables_spec.rb
index b4a0678cb5f..841cc1726a0 100644
--- a/spec/features/group_variables_spec.rb
+++ b/spec/features/group_variables_spec.rb
@@ -12,13 +12,13 @@ RSpec.describe 'Group variables', :js, feature_category: :secrets_management do
group.add_owner(user)
gitlab_sign_in(user)
- stub_feature_flags(ci_variable_drawer: false)
visit page_path
wait_for_requests
end
context 'when ci_variables_pages FF is enabled' do
- it_behaves_like 'variable list'
+ it_behaves_like 'variable list drawer'
+ it_behaves_like 'variable list env scope'
it_behaves_like 'variable list pagination', :ci_group_variable
end
@@ -27,16 +27,7 @@ RSpec.describe 'Group variables', :js, feature_category: :secrets_management do
stub_feature_flags(ci_variables_pages: false)
end
- it_behaves_like 'variable list'
- end
-
- context 'when ci_variable_drawer FF is enabled' do
- before do
- stub_feature_flags(ci_variable_drawer: true)
- visit page_path
- wait_for_requests
- end
-
it_behaves_like 'variable list drawer'
+ it_behaves_like 'variable list env scope'
end
end
diff --git a/spec/features/groups/board_sidebar_spec.rb b/spec/features/groups/board_sidebar_spec.rb
index 6a1b7d20a25..3fe520ea2ea 100644
--- a/spec/features/groups/board_sidebar_spec.rb
+++ b/spec/features/groups/board_sidebar_spec.rb
@@ -19,7 +19,6 @@ RSpec.describe 'Group Issue Boards', :js, feature_category: :groups_and_projects
let(:card) { find('.board:nth-child(1)').first('.board-card') }
before do
- stub_feature_flags(apollo_boards: false)
sign_in(user)
visit group_board_path(group, board)
diff --git a/spec/features/groups/board_spec.rb b/spec/features/groups/board_spec.rb
index c2d6b80b4c0..e6dc6055e27 100644
--- a/spec/features/groups/board_spec.rb
+++ b/spec/features/groups/board_spec.rb
@@ -14,8 +14,6 @@ RSpec.describe 'Group Boards', feature_category: :team_planning do
let_it_be(:project) { create(:project_empty_repo, group: group) }
before do
- stub_feature_flags(apollo_boards: false)
-
group.add_maintainer(user)
sign_in(user)
@@ -61,8 +59,6 @@ RSpec.describe 'Group Boards', feature_category: :team_planning do
let_it_be(:issue2) { create(:issue, title: 'issue2', project: project2) }
before do
- stub_feature_flags(apollo_boards: false)
-
project1.add_guest(user)
project2.add_reporter(user)
diff --git a/spec/features/groups/container_registry_spec.rb b/spec/features/groups/container_registry_spec.rb
index 953a8e27547..65edeb0798f 100644
--- a/spec/features/groups/container_registry_spec.rb
+++ b/spec/features/groups/container_registry_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe 'Container Registry', :js, feature_category: :container_registry do
- let(:user) { create(:user, :no_super_sidebar) }
+ let(:user) { create(:user) }
let(:group) { create(:group) }
let(:project) { create(:project, namespace: group) }
@@ -28,8 +28,8 @@ RSpec.describe 'Container Registry', :js, feature_category: :container_registry
it 'sidebar menu is open' do
visit_container_registry
- sidebar = find('.nav-sidebar')
- expect(sidebar).to have_link _('Container Registry')
+ expect(page).to have_active_navigation('Deploy')
+ expect(page).to have_active_sub_navigation('Container Registry')
end
context 'when there are no image repositories' do
diff --git a/spec/features/groups/dependency_proxy_spec.rb b/spec/features/groups/dependency_proxy_spec.rb
index 2d4f6d4fbf2..12c480a46b0 100644
--- a/spec/features/groups/dependency_proxy_spec.rb
+++ b/spec/features/groups/dependency_proxy_spec.rb
@@ -3,8 +3,8 @@
require 'spec_helper'
RSpec.describe 'Group Dependency Proxy', feature_category: :dependency_proxy do
- let(:owner) { create(:user, :no_super_sidebar) }
- let(:reporter) { create(:user, :no_super_sidebar) }
+ let(:owner) { create(:user) }
+ let(:reporter) { create(:user) }
let(:group) { create(:group) }
let(:path) { group_dependency_proxy_path(group) }
let(:settings_path) { group_settings_packages_and_registries_path(group) }
@@ -36,8 +36,8 @@ RSpec.describe 'Group Dependency Proxy', feature_category: :dependency_proxy do
it 'sidebar menu is open' do
visit path
- sidebar = find('.nav-sidebar')
- expect(sidebar).to have_link _('Dependency Proxy')
+ expect(page).to have_active_navigation('Operate')
+ expect(page).to have_active_sub_navigation('Dependency Proxy')
end
it 'toggles defaults to enabled' do
diff --git a/spec/features/groups/group_page_with_external_authorization_service_spec.rb b/spec/features/groups/group_page_with_external_authorization_service_spec.rb
index 4cc0fe4171d..fe1a685899f 100644
--- a/spec/features/groups/group_page_with_external_authorization_service_spec.rb
+++ b/spec/features/groups/group_page_with_external_authorization_service_spec.rb
@@ -2,10 +2,10 @@
require 'spec_helper'
-RSpec.describe 'The group page', feature_category: :groups_and_projects do
+RSpec.describe 'The group page', :js, feature_category: :groups_and_projects do
include ExternalAuthorizationServiceHelpers
- let(:user) { create(:user, :no_super_sidebar) }
+ let(:user) { create(:user) }
let(:group) { create(:group) }
before do
@@ -14,8 +14,9 @@ RSpec.describe 'The group page', feature_category: :groups_and_projects do
end
def expect_all_sidebar_links
- within('.nav-sidebar') do
- expect(page).to have_link('Group information')
+ within('#super-sidebar .contextual-nav') do
+ click_button 'Manage'
+ click_button 'Plan'
expect(page).to have_link('Activity')
expect(page).to have_link('Issues')
expect(page).to have_link('Merge requests')
@@ -42,8 +43,11 @@ RSpec.describe 'The group page', feature_category: :groups_and_projects do
enable_external_authorization_service_check
visit group_path(group)
- within('.nav-sidebar') do
- expect(page).to have_link('Group information')
+ within('#super-sidebar .contextual-nav') do
+ expect(page).not_to have_button('Plan')
+
+ click_button 'Manage'
+
expect(page).not_to have_link('Activity')
expect(page).not_to have_link('Contribution')
diff --git a/spec/features/groups/group_settings_spec.rb b/spec/features/groups/group_settings_spec.rb
index a248a2b471a..0437e5df6e9 100644
--- a/spec/features/groups/group_settings_spec.rb
+++ b/spec/features/groups/group_settings_spec.rb
@@ -329,7 +329,7 @@ RSpec.describe 'Edit group settings', feature_category: :groups_and_projects do
end
def updated_emails_disabled?
- group.reload.clear_memoization(:emails_disabled_memoized)
+ group.reload.clear_memoization(:emails_enabled_memoized)
group.emails_disabled?
end
end
diff --git a/spec/features/groups/members/request_access_spec.rb b/spec/features/groups/members/request_access_spec.rb
index c04b84be90e..2d0b2e483c5 100644
--- a/spec/features/groups/members/request_access_spec.rb
+++ b/spec/features/groups/members/request_access_spec.rb
@@ -3,8 +3,8 @@
require 'spec_helper'
RSpec.describe 'Groups > Members > Request access', feature_category: :groups_and_projects do
- let(:user) { create(:user, :no_super_sidebar) }
- let(:owner) { create(:user, :no_super_sidebar) }
+ let(:user) { create(:user) }
+ let(:owner) { create(:user) }
let(:group) { create(:group, :public) }
let!(:project) { create(:project, :private, namespace: group) }
@@ -48,12 +48,15 @@ RSpec.describe 'Groups > Members > Request access', feature_category: :groups_an
expect(page).not_to have_content group.name
end
- it 'user is not listed in the group members page' do
+ it 'user is not listed in the group members page', :js do
click_link 'Request Access'
expect(group.requesters.exists?(user_id: user)).to be_truthy
- first(:link, 'Members').click
+ within_testid 'super-sidebar' do
+ click_button 'Manage'
+ first(:link, 'Members').click
+ end
page.within('.content') do
expect(page).not_to have_content(user.name)
diff --git a/spec/features/groups/members/sort_members_spec.rb b/spec/features/groups/members/sort_members_spec.rb
index fd367b8e763..ea6f3ae1966 100644
--- a/spec/features/groups/members/sort_members_spec.rb
+++ b/spec/features/groups/members/sort_members_spec.rb
@@ -19,7 +19,7 @@ RSpec.describe 'Groups > Members > Sort members', :js, feature_category: :groups
def expect_sort_by(text, sort_direction)
within('[data-testid="members-sort-dropdown"]') do
expect(page).to have_css('button[aria-haspopup="menu"]', text: text)
- expect(page).to have_button("Sorting Direction: #{sort_direction == :asc ? 'Ascending' : 'Descending'}")
+ expect(page).to have_button("Sort direction: #{sort_direction == :asc ? 'Ascending' : 'Descending'}")
end
end
diff --git a/spec/features/groups/merge_requests_spec.rb b/spec/features/groups/merge_requests_spec.rb
index bbb7d322b9a..0a830e6715c 100644
--- a/spec/features/groups/merge_requests_spec.rb
+++ b/spec/features/groups/merge_requests_spec.rb
@@ -26,8 +26,10 @@ RSpec.describe 'Group merge requests page', feature_category: :code_review_workf
expect(page).not_to have_content(issuable_archived.title)
end
- it 'ignores archived merge request count badges in navbar' do
- expect(first(:link, text: 'Merge requests').find('.badge').text).to eq("1")
+ it 'ignores archived merge request count badges in navbar', :js do
+ within_testid('super-sidebar') do
+ expect(find_link(text: 'Merge requests').find('.badge').text).to eq("1")
+ end
end
it 'ignores archived merge request count badges in state-filters' do
diff --git a/spec/features/groups/navbar_spec.rb b/spec/features/groups/navbar_spec.rb
index 76e4e32d138..7d5cc704f9c 100644
--- a/spec/features/groups/navbar_spec.rb
+++ b/spec/features/groups/navbar_spec.rb
@@ -2,19 +2,19 @@
require 'spec_helper'
-RSpec.describe 'Group navbar', :with_license, feature_category: :navigation do
+RSpec.describe 'Group navbar', :with_license, :js, feature_category: :navigation do
include NavbarStructureHelper
include WikiHelpers
include_context 'group navbar structure'
- let_it_be(:user) { create(:user, :no_super_sidebar) }
+ let_it_be(:user) { create(:user) }
let(:group) { create(:group) }
before do
- insert_package_nav(_('Kubernetes'))
- insert_after_nav_item(_('Analytics'), new_nav_item: settings_for_maintainer_nav_item) if Gitlab.ee?
+ create_package_nav(_('Operate'))
+ insert_after_nav_item(_('Analyze'), new_nav_item: settings_for_maintainer_nav_item) if Gitlab.ee?
stub_config(dependency_proxy: { enabled: false })
stub_config(registry: { enabled: false })
@@ -46,9 +46,9 @@ RSpec.describe 'Group navbar', :with_license, feature_category: :navigation do
before do
if Gitlab.ee?
- insert_customer_relations_nav(_('Analytics'))
+ insert_customer_relations_nav(_('Iterations'))
else
- insert_customer_relations_nav(_('Packages and registries'))
+ insert_customer_relations_nav(_('Milestones'))
end
visit group_path(group)
@@ -85,7 +85,7 @@ RSpec.describe 'Group navbar', :with_license, feature_category: :navigation do
before do
group.update!(harbor_integration: harbor_integration)
- insert_harbor_registry_nav(_('Package Registry'))
+ insert_harbor_registry_nav(_('Kubernetes'))
visit group_path(group)
end
diff --git a/spec/features/groups/new_group_page_spec.rb b/spec/features/groups/new_group_page_spec.rb
index e1034f2bb9d..f86430ae617 100644
--- a/spec/features/groups/new_group_page_spec.rb
+++ b/spec/features/groups/new_group_page_spec.rb
@@ -12,42 +12,17 @@ RSpec.describe 'New group page', :js, feature_category: :groups_and_projects do
end
describe 'sidebar' do
- context 'in the current navigation' do
- before do
- user.update!(use_new_navigation: false)
- end
-
- context 'for a new top-level group' do
- it_behaves_like 'a "Your work" page with sidebar and breadcrumbs', :new_group_path, :groups
- end
-
- context 'for a new subgroup' do
- it 'shows the group sidebar of the parent group' do
- visit new_group_path(parent_id: parent_group.id, anchor: 'create-group-pane')
- expect(page).to have_selector(
- ".nav-sidebar[aria-label=\"Group navigation\"] .context-header[title=\"#{parent_group.name}\"]"
- )
- end
+ context 'for a new top-level group' do
+ it 'shows the "Your work" navigation' do
+ visit new_group_path
+ expect(page).to have_selector(".super-sidebar", text: "Your work")
end
end
- context 'in the new navigation' do
- before do
- user.update!(use_new_navigation: true)
- end
-
- context 'for a new top-level group' do
- it 'shows the "Your work" navigation' do
- visit new_group_path
- expect(page).to have_selector(".super-sidebar", text: "Your work")
- end
- end
-
- context 'for a new subgroup' do
- it 'shows the group navigation of the parent group' do
- visit new_group_path(parent_id: parent_group.id, anchor: 'create-group-pane')
- expect(page).to have_selector(".super-sidebar", text: parent_group.name)
- end
+ context 'for a new subgroup' do
+ it 'shows the group navigation of the parent group' do
+ visit new_group_path(parent_id: parent_group.id, anchor: 'create-group-pane')
+ expect(page).to have_selector(".super-sidebar", text: parent_group.name)
end
end
end
diff --git a/spec/features/groups/packages_spec.rb b/spec/features/groups/packages_spec.rb
index 1d9269501be..7819b1f0ab6 100644
--- a/spec/features/groups/packages_spec.rb
+++ b/spec/features/groups/packages_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe 'Group Packages', feature_category: :package_registry do
- let_it_be(:user) { create(:user, :no_super_sidebar) }
+ let_it_be(:user) { create(:user) }
let_it_be(:group) { create(:group) }
let_it_be(:project) { create(:project, group: group) }
@@ -32,7 +32,7 @@ RSpec.describe 'Group Packages', feature_category: :package_registry do
end
it 'sidebar menu is open' do
- sidebar = find('.nav-sidebar')
+ sidebar = find_by_testid('super-sidebar')
expect(sidebar).to have_link _('Package Registry')
end
diff --git a/spec/features/groups/settings/packages_and_registries_spec.rb b/spec/features/groups/settings/packages_and_registries_spec.rb
index fa310722860..cbd26441e2b 100644
--- a/spec/features/groups/settings/packages_and_registries_spec.rb
+++ b/spec/features/groups/settings/packages_and_registries_spec.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe 'Group Package and registry settings', feature_category: :package_registry do
include WaitForRequests
- let(:user) { create(:user, :no_super_sidebar) }
+ let(:user) { create(:user) }
let(:group) { create(:group) }
let(:sub_group) { create(:group, parent: group) }
@@ -20,12 +20,13 @@ RSpec.describe 'Group Package and registry settings', feature_category: :package
stub_packages_setting(enabled: false)
end
- it 'the menu item is not visible' do
+ it 'the menu item is not visible', :js do
visit group_path(group)
- settings_menu = find_settings_menu
-
- expect(settings_menu).not_to have_content 'Packages and registries'
+ within_testid('super-sidebar') do
+ click_button 'Settings'
+ expect(page).not_to have_content 'Packages and registries'
+ end
end
it 'renders 404 when navigating to page' do
@@ -36,11 +37,13 @@ RSpec.describe 'Group Package and registry settings', feature_category: :package
end
context 'when packages feature is enabled on the group' do
- it 'the menu item is visible' do
+ it 'the menu item is visible', :js do
visit group_path(group)
- settings_menu = find_settings_menu
- expect(settings_menu).to have_content 'Packages and registries'
+ within_testid('super-sidebar') do
+ click_button 'Settings'
+ expect(page).to have_content 'Packages and registries'
+ end
end
it 'has a page title set' do
@@ -49,11 +52,12 @@ RSpec.describe 'Group Package and registry settings', feature_category: :package
expect(page).to have_title _('Packages and registries settings')
end
- it 'sidebar menu is open' do
+ it 'sidebar menu is open', :js do
visit_settings_page
- sidebar = find('.nav-sidebar')
- expect(sidebar).to have_link _('Packages and registries')
+ within_testid('super-sidebar') do
+ expect(page).to have_link _('Packages and registries')
+ end
end
it 'passes axe automated accessibility testing', :js do
@@ -62,7 +66,7 @@ RSpec.describe 'Group Package and registry settings', feature_category: :package
wait_for_requests
expect(page).to be_axe_clean.within('[data-testid="packages-and-registries-group-settings"]')
- .skipping :'link-in-text-block'
+ .skipping :'link-in-text-block', :'heading-order'
end
it 'has a Duplicate packages section', :js do
@@ -124,10 +128,6 @@ RSpec.describe 'Group Package and registry settings', feature_category: :package
end
end
- def find_settings_menu
- find('.shortcuts-settings ul')
- end
-
def visit_settings_page
visit group_settings_packages_and_registries_path(group)
end
diff --git a/spec/features/groups/show_spec.rb b/spec/features/groups/show_spec.rb
index 8450322945c..cf18f3cb4e5 100644
--- a/spec/features/groups/show_spec.rb
+++ b/spec/features/groups/show_spec.rb
@@ -276,7 +276,7 @@ RSpec.describe 'Group show page', feature_category: :groups_and_projects do
end
it 'is disabled if emails are disabled' do
- group.update!(emails_disabled: true)
+ group.update!(emails_enabled: false)
visit path
diff --git a/spec/features/groups/user_sees_package_sidebar_spec.rb b/spec/features/groups/user_sees_package_sidebar_spec.rb
index 4efb9ff7608..8985b602eb7 100644
--- a/spec/features/groups/user_sees_package_sidebar_spec.rb
+++ b/spec/features/groups/user_sees_package_sidebar_spec.rb
@@ -2,8 +2,8 @@
require 'spec_helper'
-RSpec.describe 'Groups > sidebar', feature_category: :groups_and_projects do
- let(:user) { create(:user, :no_super_sidebar) }
+RSpec.describe 'Groups > sidebar', :js, feature_category: :groups_and_projects do
+ let(:user) { create(:user) }
let(:group) { create(:group) }
before do
@@ -19,13 +19,15 @@ RSpec.describe 'Groups > sidebar', feature_category: :groups_and_projects do
end
it 'shows main menu' do
- within '.nav-sidebar' do
- expect(page).to have_link(_('Packages'))
+ within_testid 'super-sidebar' do
+ click_button 'Deploy'
+ expect(page).to have_link(_('Package Registry'))
end
end
it 'has container registry link' do
- within '.nav-sidebar' do
+ within_testid 'super-sidebar' do
+ click_button 'Deploy'
expect(page).to have_link(_('Container Registry'))
end
end
@@ -38,7 +40,8 @@ RSpec.describe 'Groups > sidebar', feature_category: :groups_and_projects do
end
it 'does not have container registry link' do
- within '.nav-sidebar' do
+ within_testid 'super-sidebar' do
+ click_button 'Deploy'
expect(page).not_to have_link(_('Container Registry'))
end
end
diff --git a/spec/features/groups_spec.rb b/spec/features/groups_spec.rb
index bcbfdf487ac..578f39181d1 100644
--- a/spec/features/groups_spec.rb
+++ b/spec/features/groups_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe 'Group', feature_category: :groups_and_projects do
- let(:user) { create(:user, :no_super_sidebar) }
+ let(:user) { create(:user) }
before do
sign_in(user)
@@ -441,21 +441,30 @@ RSpec.describe 'Group', feature_category: :groups_and_projects do
expect(page).to have_content(nested_group.name)
expect(page).to have_content(project.name)
- expect(page).to have_link('Group information')
end
- it 'renders subgroup page with the text "Subgroup information"' do
+ it 'renders group page with the text "Group" in the sidebar header' do
+ visit group_path(group)
+
+ within('#super-sidebar-context-header') do
+ expect(page).to have_text('Group')
+ end
+ end
+
+ it 'renders subgroup page with the text "Group" in the sidebar header' do
visit group_path(nested_group)
- wait_for_requests
- expect(page).to have_link('Subgroup information')
+ within('#super-sidebar-context-header') do
+ expect(page).to have_text('Group')
+ end
end
- it 'renders project page with the text "Project information"' do
+ it 'renders project page with the text "Project" in the sidebar header' do
visit project_path(project)
- wait_for_requests
- expect(page).to have_link('Project information')
+ within('#super-sidebar-context-header') do
+ expect(page).to have_text('Project')
+ end
end
end
diff --git a/spec/features/help_dropdown_spec.rb b/spec/features/help_dropdown_spec.rb
index 08d7dba4d79..3e4c0bc55fe 100644
--- a/spec/features/help_dropdown_spec.rb
+++ b/spec/features/help_dropdown_spec.rb
@@ -3,24 +3,19 @@
require 'spec_helper'
RSpec.describe "Help Dropdown", :js, feature_category: :shared do
- let_it_be(:user) { create(:user, :no_super_sidebar) }
- let_it_be(:admin) { create(:admin, :no_super_sidebar) }
+ 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
+ shared_examples 'no version check badge' do
+ it 'does not render version check badge' do
+ within_testid('super-sidebar') do
+ click_on 'Help'
- expect(page).not_to have_text('Your GitLab Version')
+ 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')
@@ -28,45 +23,53 @@ RSpec.describe "Help Dropdown", :js, feature_category: :shared do
end
end
- context 'when logged in as admin' do
- before do
- sign_in(admin)
- gitlab_enable_admin_mode_sign_in(admin)
- end
+ shared_examples 'correct version check badge' do |ui_text, severity|
+ context "when severity is #{severity}" do
+ before do
+ sign_in(admin)
+ gitlab_enable_admin_mode_sign_in(admin)
- describe 'does render version data' do
- where(:response, :ui_text) do
- [
- [{ "severity" => "success" }, 'Up to date'],
- [{ "severity" => "warning" }, 'Update available'],
- [{ "severity" => "danger" }, 'Update ASAP']
- ]
+ allow_next_instance_of(VersionCheck) do |instance|
+ allow(instance).to receive(:response).and_return({ "severity" => severity })
+ end
+ visit root_path
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 check badge variant' do
+ within_testid('super-sidebar') do
+ click_on 'Help'
- it 'renders correct version badge variant',
- quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/369850' do
- page.within '.header-help' do
- find('.header-help-dropdown-toggle').click
+ 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_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_selector(
- 'a[data-testid="gitlab-version-container"][href="/help/update/index"]'
- )
- expect(page).to have_selector('.version-check-badge[href="/help/update/index"]')
- expect(page).to have_text(ui_text)
+ within page.find_link(href: help_page_path('update/index')) do
+ expect(page).to have_selector(".version-check-badge.badge-#{severity}", text: ui_text)
end
end
end
end
end
+
+ context 'when anonymous user' do
+ before do
+ visit user_path(user)
+ end
+
+ include_examples 'no version check badge'
+ end
+
+ context 'when logged in as non-admin' do
+ before do
+ sign_in(user)
+ visit root_path
+ end
+
+ include_examples 'no version check badge'
+ end
+
+ context 'when logged in as admin' do
+ include_examples 'correct version check badge', 'Up to date', 'success'
+ include_examples 'correct version check badge', 'Update available', 'warning'
+ include_examples 'correct version check badge', 'Update ASAP', 'danger'
+ end
end
diff --git a/spec/features/ide/user_opens_merge_request_spec.rb b/spec/features/ide/user_opens_merge_request_spec.rb
index 1d3cada57db..a8a56ffe310 100644
--- a/spec/features/ide/user_opens_merge_request_spec.rb
+++ b/spec/features/ide/user_opens_merge_request_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe 'IDE merge request', :js, feature_category: :web_ide do
- let_it_be(:user) { create(:user, :no_super_sidebar) }
+ let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project, :public, :repository, namespace: user.namespace) }
let_it_be(:merge_request) { create(:merge_request, :simple, source_project: project) }
@@ -16,7 +16,9 @@ RSpec.describe 'IDE merge request', :js, feature_category: :web_ide do
end
it 'user opens merge request' do
- click_button 'Code'
+ within '.merge-request' do
+ click_button 'Code'
+ end
click_link 'Open in Web IDE'
wait_for_requests
diff --git a/spec/features/invites_spec.rb b/spec/features/invites_spec.rb
index c86d4c260ee..bc6efb63f6f 100644
--- a/spec/features/invites_spec.rb
+++ b/spec/features/invites_spec.rb
@@ -59,7 +59,7 @@ RSpec.describe 'Group or Project invitations', :aggregate_failures, feature_cate
end
context 'when invite is sent before account is created;ldap or service sign in for manual acceptance edge case' do
- let(:user) { create(:user, :no_super_sidebar, email: 'user@example.com') }
+ let(:user) { create(:user, email: 'user@example.com') }
context 'when invite clicked and not signed in' do
before do
@@ -85,7 +85,6 @@ RSpec.describe 'Group or Project invitations', :aggregate_failures, feature_cate
it 'shows message user already a member' do
expect(page).to have_current_path(invite_path(group_invite.raw_invite_token), ignore_query: true)
- expect(page).to have_link(user.name, href: user_path(user))
expect(page).to have_content('You are already a member of this group.')
end
end
diff --git a/spec/features/issuables/shortcuts_issuable_spec.rb b/spec/features/issuables/shortcuts_issuable_spec.rb
index 06387c14ee2..6bb453c34e6 100644
--- a/spec/features/issuables/shortcuts_issuable_spec.rb
+++ b/spec/features/issuables/shortcuts_issuable_spec.rb
@@ -58,14 +58,13 @@ RSpec.describe 'Blob shortcuts', :js, feature_category: :team_planning do
it "opens assignee dropdown for editing" do
find('body').native.send_key('a')
- expect(find('.block.assignee')).to have_selector('.js-sidebar-assignee-data')
+ expect(find('.block.assignee')).to have_selector('.dropdown-menu-user')
end
end
describe 'pressing "a"' do
describe 'On an Issue' do
before do
- stub_feature_flags(issue_assignees_widget: false)
visit project_issue_path(project, issue)
wait_for_requests
end
@@ -75,7 +74,6 @@ RSpec.describe 'Blob shortcuts', :js, feature_category: :team_planning do
describe 'On a Merge Request' do
before do
- stub_feature_flags(issue_assignees_widget: false)
visit project_merge_request_path(project, merge_request)
wait_for_requests
end
diff --git a/spec/features/issues/discussion_lock_spec.rb b/spec/features/issues/discussion_lock_spec.rb
index fb9addff1a2..04d59854ddc 100644
--- a/spec/features/issues/discussion_lock_spec.rb
+++ b/spec/features/issues/discussion_lock_spec.rb
@@ -28,7 +28,7 @@ RSpec.describe 'Discussion Lock', :js, feature_category: :team_planning do
click_button('Lock')
end
- expect(find('#notes')).to have_content('locked this issue')
+ expect(find('#notes')).to have_content('locked the discussion in this issue')
end
end
@@ -46,7 +46,7 @@ RSpec.describe 'Discussion Lock', :js, feature_category: :team_planning do
click_button('Unlock')
end
- expect(find('#notes')).to have_content('unlocked this issue')
+ expect(find('#notes')).to have_content('unlocked the discussion in this issue')
expect(find('.issuable-sidebar')).to have_content('Unlocked')
end
@@ -101,7 +101,7 @@ RSpec.describe 'Discussion Lock', :js, feature_category: :team_planning do
page.within('#notes') do
expect(page).not_to have_selector('js-main-target-form')
expect(page.find('.disabled-comments'))
- .to have_content('This issue is locked. Only project members can comment.')
+ .to have_content('The discussion in this issue is locked. Only project members can comment.')
end
end
end
diff --git a/spec/features/issues/filtered_search/visual_tokens_spec.rb b/spec/features/issues/filtered_search/visual_tokens_spec.rb
index e51c82081ff..ca1a822fd88 100644
--- a/spec/features/issues/filtered_search/visual_tokens_spec.rb
+++ b/spec/features/issues/filtered_search/visual_tokens_spec.rb
@@ -6,8 +6,8 @@ RSpec.describe 'Visual tokens', :js, feature_category: :team_planning do
include FilteredSearchHelpers
let_it_be(:project) { create(:project) }
- let_it_be(:user) { create(:user, :no_super_sidebar, name: 'administrator', username: 'root') }
- let_it_be(:user_rock) { create(:user, :no_super_sidebar, name: 'The Rock', username: 'rock') }
+ let_it_be(:user) { create(:user, name: 'administrator', username: 'root') }
+ let_it_be(:user_rock) { create(:user, name: 'The Rock', username: 'rock') }
let_it_be(:milestone_nine) { create(:milestone, title: '9.0', project: project) }
let_it_be(:milestone_ten) { create(:milestone, title: '10.0', project: project) }
let_it_be(:label) { create(:label, project: project, title: 'abc') }
@@ -41,7 +41,7 @@ RSpec.describe 'Visual tokens', :js, feature_category: :team_planning do
end
it 'ends editing mode when document is clicked' do
- find('.js-navbar').click
+ find('body').click(x: 0, y: 0)
expect_empty_search_term
expect_hidden_suggestions_list
diff --git a/spec/features/issues/form_spec.rb b/spec/features/issues/form_spec.rb
index 73c53e855b2..2fb30469691 100644
--- a/spec/features/issues/form_spec.rb
+++ b/spec/features/issues/form_spec.rb
@@ -7,9 +7,9 @@ RSpec.describe 'New/edit issue', :js, feature_category: :team_planning do
include ListboxHelpers
let_it_be(:project) { create(:project, :repository) }
- let_it_be(:user) { create(:user, :no_super_sidebar) }
- let_it_be(:user2) { create(:user, :no_super_sidebar) }
- let_it_be(:guest) { create(:user, :no_super_sidebar) }
+ let_it_be(:user) { create(:user) }
+ let_it_be(:user2) { create(:user) }
+ let_it_be(:guest) { create(:user) }
let_it_be(:milestone) { create(:milestone, project: project) }
let_it_be(:label) { create(:label, project: project) }
let_it_be(:label2) { create(:label, project: project) }
@@ -526,7 +526,7 @@ RSpec.describe 'New/edit issue', :js, feature_category: :team_planning do
find('body').send_keys('e')
- click_link 'Boards'
+ click_link 'Homepage'
expect(page).not_to have_content(expected_content)
end
@@ -539,7 +539,7 @@ RSpec.describe 'New/edit issue', :js, feature_category: :team_planning do
find('body').send_keys('e')
fill_in 'issue-description', with: content
- click_link 'Boards' do
+ click_link 'Homepage' do
page.driver.browser.switch_to.alert.dismiss
end
@@ -554,8 +554,8 @@ RSpec.describe 'New/edit issue', :js, feature_category: :team_planning do
find('body').send_keys('e')
fill_in 'issue-description', with: content
- click_link 'Boards' do
- page.driver.browser.switch_to.alert.accept
+ click_link 'Homepage' do
+ page.driver.browser.switch_to.alert.dismiss
end
expect(page).not_to have_content(content)
@@ -601,14 +601,4 @@ RSpec.describe 'New/edit issue', :js, feature_category: :team_planning do
end
end
end
-
- def before_for_selector(selector)
- js = <<-JS.strip_heredoc
- (function(selector) {
- var el = document.querySelector(selector);
- return window.getComputedStyle(el, '::before').getPropertyValue('content');
- })("#{escape_javascript(selector)}")
- JS
- page.evaluate_script(js)
- end
end
diff --git a/spec/features/issues/issue_sidebar_spec.rb b/spec/features/issues/issue_sidebar_spec.rb
index a015a83c793..e4df106de07 100644
--- a/spec/features/issues/issue_sidebar_spec.rb
+++ b/spec/features/issues/issue_sidebar_spec.rb
@@ -26,179 +26,66 @@ RSpec.describe 'Issue Sidebar', feature_category: :team_planning do
let(:user2) { create(:user) }
let(:issue2) { create(:issue, project: project, author: user2) }
- context 'when GraphQL assignees widget feature flag is disabled' do
- before do
- stub_feature_flags(issue_assignees_widget: false)
- end
+ include_examples 'issuable invite members' do
+ let(:issuable_path) { project_issue_path(project, issue2) }
+ end
- include_examples 'issuable invite members' do
- let(:issuable_path) { project_issue_path(project, issue2) }
+ context 'when user is a developer' do
+ before do
+ project.add_developer(user)
+ visit_issue(project, issue2)
end
- context 'when user is a developer' do
- before do
- project.add_developer(user)
- visit_issue(project, issue2)
-
- find('.block.assignee .edit-link').click
- wait_for_requests
- end
-
- it 'shows author in assignee dropdown' do
- page.within '.dropdown-menu-user' do
- expect(page).to have_content(user2.name)
- end
- end
-
- it 'shows author when filtering assignee dropdown' do
- page.within '.dropdown-menu-user' do
- find('.dropdown-input-field').set(user2.name)
-
- wait_for_requests
-
- expect(page).to have_content(user2.name)
- end
- end
-
- it 'assigns yourself' do
- find('.block.assignee .dropdown-menu-toggle').click
-
- click_button 'assign yourself'
-
- wait_for_requests
-
- find('.block.assignee .edit-link').click
-
- page.within '.dropdown-menu-user' do
- expect(page.find('.dropdown-header')).to be_visible
- expect(page.find('.dropdown-menu-user-link.is-active')).to have_content(user.name)
- end
- end
-
- it 'keeps your filtered term after filtering and dismissing the dropdown' do
- find('.dropdown-input-field').set(user2.name)
-
- wait_for_requests
-
- page.within '.dropdown-menu-user' do
- expect(page).not_to have_content 'Unassigned'
- click_link user2.name
- end
-
- within '.js-right-sidebar' do
- find('.block.assignee').click(x: 0, y: 0, offset: 0)
- find('.block.assignee .edit-link').click
- end
-
- expect(page.all('.dropdown-menu-user li').length).to eq(6)
- expect(find('.dropdown-input-field').value).to eq('')
- end
-
- it 'shows label text as "Apply" when assignees are changed' do
- project.add_developer(user)
- visit_issue(project, issue2)
-
- find('.block.assignee .edit-link').click
- wait_for_requests
+ it 'shows author in assignee dropdown' do
+ open_assignees_dropdown
- click_on 'Unassigned'
-
- expect(page).to have_link('Apply')
+ page.within '.dropdown-menu-user' do
+ expect(page).to have_content(user2.name)
end
end
- end
-
- context 'when GraphQL assignees widget feature flag is enabled' do
- # TODO: Move to shared examples when feature flag is removed: https://gitlab.com/gitlab-org/gitlab/-/issues/328185
- context 'when a privileged user can invite' do
- it 'shows a link for inviting members and launches invite modal' do
- project.add_maintainer(user)
- visit_issue(project, issue2)
- open_assignees_dropdown
+ it 'shows author when filtering assignee dropdown' do
+ open_assignees_dropdown
- page.within '.dropdown-menu-user' do
- expect(page).to have_link('Invite members')
+ page.within '.dropdown-menu-user' do
+ find('[data-testid="user-search-input"]').set(user2.name)
- click_link 'Invite members'
- end
+ wait_for_requests
- page.within invite_modal_selector do
- expect(page).to have_content("You're inviting members to the #{project.name} project")
- end
+ expect(page).to have_content(user2.name)
end
end
- context 'when user cannot invite members in assignee dropdown' do
- it 'shows author in assignee dropdown and no invite link' do
- project.add_developer(user)
- visit_issue(project, issue2)
-
- open_assignees_dropdown
+ it 'assigns yourself' do
+ click_button 'assign yourself'
+ wait_for_requests
- page.within '.dropdown-menu-user' do
- expect(page).not_to have_link('Invite members')
- end
+ page.within '.assignee' do
+ expect(page).to have_content(user.name)
end
end
- context 'when user is a developer' do
- before do
- project.add_developer(user)
- visit_issue(project, issue2)
- end
+ it 'keeps your filtered term after filtering and dismissing the dropdown' do
+ open_assignees_dropdown
- it 'shows author in assignee dropdown' do
- open_assignees_dropdown
+ find('[data-testid="user-search-input"]').set(user2.name)
+ wait_for_requests
- page.within '.dropdown-menu-user' do
- expect(page).to have_content(user2.name)
- end
+ page.within '.dropdown-menu-user' do
+ expect(page).not_to have_content 'Unassigned'
+ click_button user2.name
end
- it 'shows author when filtering assignee dropdown' do
- open_assignees_dropdown
-
- page.within '.dropdown-menu-user' do
- find('[data-testid="user-search-input"]').set(user2.name)
-
- wait_for_requests
+ find('.participants').click
+ wait_for_requests
- expect(page).to have_content(user2.name)
- end
- end
-
- it 'assigns yourself' do
- click_button 'assign yourself'
- wait_for_requests
+ open_assignees_dropdown
- page.within '.assignee' do
- expect(page).to have_content(user.name)
- end
+ page.within('.assignee') do
+ expect(page.all('[data-testid="selected-participant"]').length).to eq(1)
end
- it 'keeps your filtered term after filtering and dismissing the dropdown' do
- open_assignees_dropdown
-
- find('[data-testid="user-search-input"]').set(user2.name)
- wait_for_requests
-
- page.within '.dropdown-menu-user' do
- expect(page).not_to have_content 'Unassigned'
- click_button user2.name
- end
-
- find('.participants').click
- wait_for_requests
-
- open_assignees_dropdown
-
- page.within('.assignee') do
- expect(page.all('[data-testid="selected-participant"]').length).to eq(1)
- end
-
- expect(find('[data-testid="user-search-input"]').value).to eq(user2.name)
- end
+ expect(find('[data-testid="user-search-input"]').value).to eq(user2.name)
end
end
end
diff --git a/spec/features/issues/issue_state_spec.rb b/spec/features/issues/issue_state_spec.rb
index 3fe49ff7080..125329764c6 100644
--- a/spec/features/issues/issue_state_spec.rb
+++ b/spec/features/issues/issue_state_spec.rb
@@ -51,7 +51,7 @@ RSpec.describe 'issue state', :js, feature_category: :team_planning do
find('#new-actions-header-dropdown > button').click
end
- it_behaves_like 'issue closed', '.dropdown-menu-right'
+ it_behaves_like 'issue closed', '.gl-new-dropdown-contents'
end
context 'when clicking the bottom `Close issue` button', :aggregate_failures do
@@ -74,7 +74,7 @@ RSpec.describe 'issue state', :js, feature_category: :team_planning do
find('#new-actions-header-dropdown > button').click
end
- it_behaves_like 'issue reopened', '.dropdown-menu-right'
+ it_behaves_like 'issue reopened', '.gl-new-dropdown-contents'
end
context 'when clicking the bottom `Reopen issue` button', :aggregate_failures do
diff --git a/spec/features/issues/move_spec.rb b/spec/features/issues/move_spec.rb
index 4a38373db71..eca52a3a2c3 100644
--- a/spec/features/issues/move_spec.rb
+++ b/spec/features/issues/move_spec.rb
@@ -44,7 +44,7 @@ RSpec.describe 'issue move to another project', feature_category: :team_planning
it 'moving issue to another project', :js do
click_button _('Move issue')
wait_for_requests
- all('.gl-dropdown-item')[0].click
+ all('.gl-new-dropdown-item')[0].click
click_button _('Move')
expect(page).to have_content("Text with #{cross_reference}#{mr.to_reference}")
@@ -116,7 +116,7 @@ RSpec.describe 'issue move to another project', feature_category: :team_planning
click_button _('Move issue')
wait_for_requests
- find('.gl-dropdown-item', text: project_title).click
+ find('.gl-new-dropdown-item', text: project_title).click
click_button _('Move')
end
diff --git a/spec/features/issues/service_desk_spec.rb b/spec/features/issues/service_desk_spec.rb
index 8e952a23f05..8662f0f98f5 100644
--- a/spec/features/issues/service_desk_spec.rb
+++ b/spec/features/issues/service_desk_spec.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe 'Service Desk Issue Tracker', :js, feature_category: :service_desk do
let(:project) { create(:project, :private, service_desk_enabled: true) }
- let_it_be(:user) { create(:user, :no_super_sidebar) }
+ let_it_be(:user) { create(:user) }
let_it_be(:support_bot) { Users::Internal.support_bot }
before do
@@ -21,8 +21,10 @@ RSpec.describe 'Service Desk Issue Tracker', :js, feature_category: :service_des
describe 'navigation to service desk' do
before do
visit project_path(project)
- find('.sidebar-top-level-items .shortcuts-issues').click
- find('.sidebar-sub-level-items a', text: 'Service Desk').click
+ find('#menu-section-button-monitor').click
+ within('#monitor') do
+ click_link('Service Desk')
+ end
end
it 'can navigate to the service desk from link in the sidebar' do
diff --git a/spec/features/issues/todo_spec.rb b/spec/features/issues/todo_spec.rb
index 2095453ac29..458e3fac517 100644
--- a/spec/features/issues/todo_spec.rb
+++ b/spec/features/issues/todo_spec.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe 'Manually create a todo item from issue', :js, feature_category: :team_planning do
let!(:project) { create(:project) }
let!(:issue) { create(:issue, project: project) }
- let!(:user) { create(:user, :no_super_sidebar) }
+ let!(:user) { create(:user) }
before do
stub_feature_flags(notifications_todos_buttons: false)
@@ -20,13 +20,13 @@ RSpec.describe 'Manually create a todo item from issue', :js, feature_category:
expect(page).to have_content 'Mark as done'
end
- page.within ".header-content span[aria-label='#{_('Todos count')}']" do
+ within_testid 'todos-shortcut-button' do
expect(page).to have_content '1'
end
visit project_issue_path(project, issue)
- page.within ".header-content span[aria-label='#{_('Todos count')}']" do
+ within_testid 'todos-shortcut-button' do
expect(page).to have_content '1'
end
end
@@ -37,10 +37,10 @@ RSpec.describe 'Manually create a todo item from issue', :js, feature_category:
click_button 'Mark as done'
end
- expect(page).to have_selector(".header-content span[aria-label='#{_('Todos count')}']", visible: false)
+ expect(page).to have_selector("[data-testid='todos-shortcut-button']", text: '')
visit project_issue_path(project, issue)
- expect(page).to have_selector(".header-content span[aria-label='#{_('Todos count')}']", visible: false)
+ expect(page).to have_selector("[data-testid='todos-shortcut-button']", text: '')
end
end
diff --git a/spec/features/issues/user_comments_on_issue_spec.rb b/spec/features/issues/user_comments_on_issue_spec.rb
index a81a99771cc..d27f3ffebe6 100644
--- a/spec/features/issues/user_comments_on_issue_spec.rb
+++ b/spec/features/issues/user_comments_on_issue_spec.rb
@@ -33,7 +33,9 @@ RSpec.describe "User comments on issue", :js, feature_category: :team_planning d
end
end
- it_behaves_like 'edits content using the content editor'
+ # do not test quick actions here since guest users don't have permission
+ # to execute all quick actions
+ it_behaves_like 'edits content using the content editor', { with_quick_actions: false }
it "adds comment with code block" do
code_block_content = "Command [1]: /usr/local/bin/git , see [text](doc/text)"
diff --git a/spec/features/issues/user_creates_issue_spec.rb b/spec/features/issues/user_creates_issue_spec.rb
index 29b44bf165d..a407e7fd112 100644
--- a/spec/features/issues/user_creates_issue_spec.rb
+++ b/spec/features/issues/user_creates_issue_spec.rb
@@ -139,8 +139,6 @@ RSpec.describe "User creates issue", feature_category: :team_planning do
end
end
- it_behaves_like 'edits content using the content editor'
-
context 'dropzone upload file', :js do
before do
visit new_project_issue_path(project)
@@ -308,6 +306,21 @@ RSpec.describe "User creates issue", feature_category: :team_planning do
end
end
+ context 'when signed in as a maintainer', :js do
+ let_it_be(:project) { create(:project) }
+
+ before_all do
+ project.add_maintainer(user)
+ end
+
+ before do
+ sign_in(user)
+ visit(new_project_issue_path(project))
+ end
+
+ it_behaves_like 'edits content using the content editor'
+ end
+
context "when signed in as user with special characters in their name" do
let(:user_special) { create(:user, name: "Jon O'Shea") }
diff --git a/spec/features/issues/user_edits_issue_spec.rb b/spec/features/issues/user_edits_issue_spec.rb
index 7919e8f7ed4..e9bf1ef542b 100644
--- a/spec/features/issues/user_edits_issue_spec.rb
+++ b/spec/features/issues/user_edits_issue_spec.rb
@@ -210,166 +210,79 @@ RSpec.describe "Issues > User edits issue", :js, feature_category: :team_plannin
end
describe 'update assignee' do
- context 'when GraphQL assignees widget feature flag is disabled' do
- before do
- stub_feature_flags(issue_assignees_widget: false)
- end
-
- context 'by authorized user' do
- def close_dropdown_menu_if_visible
- find('.dropdown-menu-toggle', visible: :all).tap do |toggle|
- toggle.click if toggle.visible?
- end
- end
-
- it 'allows user to select unassigned' do
- visit project_issue_path(project, issue)
-
- page.within('.assignee') do
- expect(page).to have_content user.name.to_s
-
- click_link 'Edit'
- click_link 'Unassigned'
-
- close_dropdown_menu_if_visible
-
- expect(page).to have_content 'None - assign yourself'
- end
- end
-
- it 'allows user to select an assignee' do
- issue2 = create(:issue, project: project, author: user)
- visit project_issue_path(project, issue2)
-
- page.within('.assignee') do
- expect(page).to have_content "None"
- end
-
- page.within '.assignee' do
- click_link 'Edit'
- end
-
- page.within '.dropdown-menu-user' do
- click_link user.name
- end
-
- page.within('.assignee') do
- expect(page).to have_content user.name
- end
- end
-
- it 'allows user to unselect themselves' do
- issue2 = create(:issue, project: project, author: user, assignees: [user])
-
- visit project_issue_path(project, issue2)
+ context 'by authorized user' do
+ it 'allows user to select unassigned' do
+ visit project_issue_path(project, issue)
- page.within '.assignee' do
- expect(page).to have_content user.name
+ page.within('.assignee') do
+ expect(page).to have_content user.name.to_s
- click_link 'Edit'
- click_link user.name
+ click_button('Edit')
+ wait_for_requests
- close_dropdown_menu_if_visible
+ find('[data-testid="unassign"]').click
+ find('[data-testid="title"]').click
+ wait_for_requests
- page.within '[data-testid="no-value"]' do
- expect(page).to have_content "None"
- end
- end
+ expect(page).to have_content 'None - assign yourself'
end
end
- context 'by unauthorized user' do
- let(:guest) { create(:user) }
-
- before do
- project.add_guest(guest)
- end
-
- it 'shows assignee text' do
- sign_out(:user)
- sign_in(guest)
+ it 'allows user to select an assignee' do
+ issue2 = create(:issue, project: project, author: user)
+ visit project_issue_path(project, issue2)
- visit project_issue_path(project, issue)
- expect(page).to have_content issue.assignees.first.name
+ page.within('.assignee') do
+ expect(page).to have_content "None"
+ click_button('Edit')
+ wait_for_requests
end
- end
- end
-
- context 'when GraphQL assignees widget feature flag is enabled' do
- context 'by authorized user' do
- it 'allows user to select unassigned' do
- visit project_issue_path(project, issue)
-
- page.within('.assignee') do
- expect(page).to have_content user.name.to_s
-
- click_button('Edit')
- wait_for_requests
-
- find('[data-testid="unassign"]').click
- find('[data-testid="title"]').click
- wait_for_requests
- expect(page).to have_content 'None - assign yourself'
- end
+ page.within '.dropdown-menu-user' do
+ click_button user.name
end
- it 'allows user to select an assignee' do
- issue2 = create(:issue, project: project, author: user)
- visit project_issue_path(project, issue2)
-
- page.within('.assignee') do
- expect(page).to have_content "None"
- click_button('Edit')
- wait_for_requests
- end
-
- page.within '.dropdown-menu-user' do
- click_button user.name
- end
-
- page.within('.assignee') do
- find('[data-testid="title"]').click
- wait_for_requests
+ page.within('.assignee') do
+ find('[data-testid="title"]').click
+ wait_for_requests
- expect(page).to have_content user.name
- end
+ expect(page).to have_content user.name
end
+ end
- it 'allows user to unselect themselves' do
- issue2 = create(:issue, project: project, author: user, assignees: [user])
+ it 'allows user to unselect themselves' do
+ issue2 = create(:issue, project: project, author: user, assignees: [user])
- visit project_issue_path(project, issue2)
+ visit project_issue_path(project, issue2)
- page.within '.assignee' do
- expect(page).to have_content user.name
+ page.within '.assignee' do
+ expect(page).to have_content user.name
- click_button('Edit')
- wait_for_requests
- click_button user.name
+ click_button('Edit')
+ wait_for_requests
+ click_button user.name
- find('[data-testid="title"]').click
- wait_for_requests
+ find('[data-testid="title"]').click
+ wait_for_requests
- expect(page).to have_content "None"
- end
+ expect(page).to have_content "None"
end
end
+ end
- context 'by unauthorized user' do
- let(:guest) { create(:user) }
+ context 'by unauthorized user' do
+ let(:guest) { create(:user) }
- before do
- project.add_guest(guest)
- end
+ before do
+ project.add_guest(guest)
+ end
- it 'shows assignee text' do
- sign_out(:user)
- sign_in(guest)
+ it 'shows assignee text' do
+ sign_out(:user)
+ sign_in(guest)
- visit project_issue_path(project, issue)
- expect(page).to have_content issue.assignees.first.name
- end
+ visit project_issue_path(project, issue)
+ expect(page).to have_content issue.assignees.first.name
end
end
end
diff --git a/spec/features/issues/user_interacts_with_awards_spec.rb b/spec/features/issues/user_interacts_with_awards_spec.rb
index 539e429534e..813fdeea0a1 100644
--- a/spec/features/issues/user_interacts_with_awards_spec.rb
+++ b/spec/features/issues/user_interacts_with_awards_spec.rb
@@ -57,7 +57,7 @@ RSpec.describe 'User interacts with awards', feature_category: :team_planning do
end
page.within('.emoji-picker') do
- emoji_button = page.first('gl-emoji[data-name="8ball"]')
+ emoji_button = page.first('gl-emoji[data-name="grinning"]')
emoji_button.hover
emoji_button.click
end
@@ -65,7 +65,7 @@ RSpec.describe 'User interacts with awards', feature_category: :team_planning do
page.within('.awards') do
expect(page).to have_selector('[data-testid="award-button"]')
expect(page.find('[data-testid="award-button"].selected .js-counter')).to have_content('1')
- expect(page).to have_css('[data-testid="award-button"].selected[title="You reacted with :8ball:"]')
+ expect(page).to have_css('[data-testid="award-button"].selected[title="You reacted with :grinning:"]')
wait_for_requests
@@ -114,17 +114,17 @@ RSpec.describe 'User interacts with awards', feature_category: :team_planning do
context 'User interacts with awards on a note' do
let!(:note) { create(:note, noteable: issue, project: issue.project) }
- let!(:award_emoji) { create(:award_emoji, awardable: note, name: '100') }
+ let!(:award_emoji) { create(:award_emoji, awardable: note, name: 'grinning') }
it 'shows the award on the note' do
page.within('.note-awards') do
- expect(page).to have_emoji('100')
+ expect(page).to have_emoji('grinning')
end
end
it 'allows adding a vote to an award' do
page.within('.note-awards') do
- find('gl-emoji[data-name="100"]').click
+ find('gl-emoji[data-name="grinning"]').click
end
wait_for_requests
@@ -140,11 +140,11 @@ RSpec.describe 'User interacts with awards', feature_category: :team_planning do
# make sure emoji popup is visible
execute_script("window.scrollBy(0, 200)")
- find('gl-emoji[data-name="8ball"]').click
+ find('gl-emoji[data-name="laughing"]').click
wait_for_requests
page.within('.note-awards') do
- expect(page).to have_emoji('8ball')
+ expect(page).to have_emoji('laughing')
end
expect(note.reload.award_emoji.size).to eq(2)
restore_window_size
@@ -165,7 +165,7 @@ RSpec.describe 'User interacts with awards', feature_category: :team_planning do
it 'does not allow toggling existing emoji' do
page.within('.note-awards') do
- find('gl-emoji[data-name="100"]').click
+ find('gl-emoji[data-name="grinning"]').click
end
wait_for_requests
diff --git a/spec/features/issues/user_uses_quick_actions_spec.rb b/spec/features/issues/user_uses_quick_actions_spec.rb
index d3552b87fea..937a0683794 100644
--- a/spec/features/issues/user_uses_quick_actions_spec.rb
+++ b/spec/features/issues/user_uses_quick_actions_spec.rb
@@ -12,8 +12,8 @@ RSpec.describe 'Issues > User uses quick actions', :js, feature_category: :team_
context "issuable common quick actions" do
let(:new_url_opts) { {} }
- let(:maintainer) { create(:user, :no_super_sidebar) }
- let(:project) { create(:project, :public) }
+ let(:maintainer) { create(:user) }
+ let_it_be(:project) { create(:project, :public) }
let!(:label_bug) { create(:label, project: project, title: 'bug') }
let!(:label_feature) { create(:label, project: project, title: 'feature') }
let!(:milestone) { create(:milestone, project: project, title: 'ASAP') }
@@ -25,7 +25,7 @@ RSpec.describe 'Issues > User uses quick actions', :js, feature_category: :team_
end
describe 'issue-only commands' do
- let(:user) { create(:user, :no_super_sidebar) }
+ let(:user) { create(:user) }
let(:project) { create(:project, :public, :repository) }
let(:issue) { create(:issue, project: project, due_date: Date.new(2016, 8, 28)) }
diff --git a/spec/features/jira_connect/branches_spec.rb b/spec/features/jira_connect/branches_spec.rb
index ae1dd551c47..25dc14a1dc9 100644
--- a/spec/features/jira_connect/branches_spec.rb
+++ b/spec/features/jira_connect/branches_spec.rb
@@ -5,8 +5,8 @@ require 'spec_helper'
RSpec.describe 'Create GitLab branches from Jira', :js, feature_category: :integrations do
include ListboxHelpers
- let_it_be(:alice) { create(:user, :no_super_sidebar, name: 'Alice') }
- let_it_be(:bob) { create(:user, :no_super_sidebar, name: 'Bob') }
+ let_it_be(:alice) { create(:user, name: 'Alice') }
+ let_it_be(:bob) { create(:user, name: 'Bob') }
let_it_be(:project1) { create(:project, :repository, namespace: alice.namespace, title: 'foo') }
let_it_be(:project2) { create(:project, :repository, namespace: alice.namespace, title: 'bar') }
diff --git a/spec/features/jira_oauth_provider_authorize_spec.rb b/spec/features/jira_oauth_provider_authorize_spec.rb
deleted file mode 100644
index e873d9c219f..00000000000
--- a/spec/features/jira_oauth_provider_authorize_spec.rb
+++ /dev/null
@@ -1,42 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe 'JIRA OAuth Provider', feature_category: :integrations do
- describe 'JIRA DVCS OAuth Authorization' do
- let_it_be(:application) do
- create(:oauth_application, redirect_uri: oauth_jira_dvcs_callback_url, scopes: 'read_user')
- end
-
- let(:authorize_path) do
- oauth_jira_dvcs_authorize_path(client_id: application.uid,
- redirect_uri: oauth_jira_dvcs_callback_url,
- response_type: 'code',
- state: 'my_state',
- scope: 'read_user')
- end
-
- before do
- sign_in(user)
- end
-
- it_behaves_like 'Secure OAuth Authorizations' do
- before do
- visit authorize_path
- end
- end
-
- context 'when the flag is disabled' do
- let_it_be(:user) { create(:user) }
-
- before do
- stub_feature_flags(jira_dvcs_end_of_life_amnesty: false)
- visit authorize_path
- end
-
- it 'presents as an endpoint that does not exist' do
- expect(page).to have_gitlab_http_status(:not_found)
- end
- end
- end
-end
diff --git a/spec/features/labels_hierarchy_spec.rb b/spec/features/labels_hierarchy_spec.rb
index 72f5b46c3ad..c6c7342325b 100644
--- a/spec/features/labels_hierarchy_spec.rb
+++ b/spec/features/labels_hierarchy_spec.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe 'Labels Hierarchy', :js, feature_category: :team_planning do
include FilteredSearchHelpers
- let!(:user) { create(:user, :no_super_sidebar) }
+ let!(:user) { create(:user) }
let!(:grandparent) { create(:group) }
let!(:parent) { create(:group, parent: grandparent) }
let!(:child) { create(:group, parent: parent) }
@@ -179,7 +179,7 @@ RSpec.describe 'Labels Hierarchy', :js, feature_category: :team_planning do
wait_for_requests
end
- find('.btn-confirm').click
+ click_button 'Create issue'
expect(page.find('.issue-details h1.title')).to have_content('new created issue')
expect(page).to have_selector('span.gl-label-text', text: grandparent_group_label.title)
diff --git a/spec/features/markdown/keyboard_shortcuts_spec.rb b/spec/features/markdown/keyboard_shortcuts_spec.rb
index 6f128e16041..ba88278199a 100644
--- a/spec/features/markdown/keyboard_shortcuts_spec.rb
+++ b/spec/features/markdown/keyboard_shortcuts_spec.rb
@@ -97,8 +97,8 @@ RSpec.describe 'Markdown keyboard shortcuts', :js, feature_category: :team_plann
context 'Vue.js markdown editor' do
let(:path_to_visit) { new_project_release_path(project) }
- let(:markdown_field) { find_field('Release notes') }
- let(:non_markdown_field) { find_field('Release title') }
+ let(:markdown_field) { find_field('release-notes') }
+ let(:non_markdown_field) { find_field('release-title') }
it_behaves_like 'keyboard shortcuts'
it_behaves_like 'no side effects'
diff --git a/spec/features/merge_request/maintainer_edits_fork_spec.rb b/spec/features/merge_request/maintainer_edits_fork_spec.rb
index 7603696c60c..8618dca5873 100644
--- a/spec/features/merge_request/maintainer_edits_fork_spec.rb
+++ b/spec/features/merge_request/maintainer_edits_fork_spec.rb
@@ -50,7 +50,10 @@ RSpec.describe 'a maintainer edits files on a source-branch of an MR from a fork
click_button 'Commit changes'
wait_for_requests
- expect(page).to have_content('Your changes have been successfully committed')
+ expect(page).to have_content('Your changes have been committed successfully')
+ page.within '.flash-container' do
+ expect(page).to have_link 'changes'
+ end
expect(page).to have_content(content)
end
end
diff --git a/spec/features/merge_request/merge_request_discussion_lock_spec.rb b/spec/features/merge_request/merge_request_discussion_lock_spec.rb
index 782c4af58ac..7e01063816f 100644
--- a/spec/features/merge_request/merge_request_discussion_lock_spec.rb
+++ b/spec/features/merge_request/merge_request_discussion_lock_spec.rb
@@ -92,7 +92,7 @@ RSpec.describe 'Merge Request Discussion Lock', :js, feature_category: :code_rev
it 'the user can lock the merge_request' do
find('#new-actions-header-dropdown button').click
- expect(page).to have_content('Lock merge request')
+ expect(page).to have_content('Lock discussion')
end
end
@@ -105,7 +105,7 @@ RSpec.describe 'Merge Request Discussion Lock', :js, feature_category: :code_rev
it 'the user can unlock the merge_request' do
find('#new-actions-header-dropdown button').click
- expect(page).to have_content('Unlock merge request')
+ expect(page).to have_content('Unlock discussion')
end
end
end
diff --git a/spec/features/merge_request/user_awards_emoji_spec.rb b/spec/features/merge_request/user_awards_emoji_spec.rb
index f43672942ff..63e1cffed23 100644
--- a/spec/features/merge_request/user_awards_emoji_spec.rb
+++ b/spec/features/merge_request/user_awards_emoji_spec.rb
@@ -50,7 +50,7 @@ RSpec.describe 'Merge request > User awards emoji', :js, feature_category: :code
# make sure emoji popup is visible
execute_script("window.scrollBy(0, 200)")
- find('gl-emoji[data-name="8ball"]').click
+ find('gl-emoji[data-name="grinning"]').click
end
wait_for_requests
diff --git a/spec/features/merge_request/user_edits_assignees_sidebar_spec.rb b/spec/features/merge_request/user_edits_assignees_sidebar_spec.rb
index 2fcbb4e70c3..ae95bc3e11f 100644
--- a/spec/features/merge_request/user_edits_assignees_sidebar_spec.rb
+++ b/spec/features/merge_request/user_edits_assignees_sidebar_spec.rb
@@ -34,202 +34,81 @@ RSpec.describe 'Merge request > User edits assignees sidebar', :js, feature_cate
let(:sidebar_assignee_tooltip) { sidebar_assignee_avatar_link['title'] || '' }
let(:sidebar_assignee_merge_ability) { sidebar_assignee_avatar_link['data-cannot-merge'] || '' }
- context 'when GraphQL assignees widget feature flag is disabled' do
- let(:sidebar_assignee_dropdown_item) do
- sidebar_assignee_block.find(".dropdown-menu li[data-user-id=\"#{assignee.id}\"]")
- end
-
- let(:sidebar_assignee_dropdown_tooltip) { sidebar_assignee_dropdown_item.find('a')['data-title'] || '' }
+ let(:sidebar_assignee_dropdown_item) { sidebar_assignee_block.find(".dropdown-item", text: assignee.username) }
+ let(:sidebar_assignee_dropdown_tooltip) { sidebar_assignee_dropdown_item['title'] }
+ context 'when user is an owner' do
before do
- stub_feature_flags(issue_assignees_widget: false)
- end
+ stub_const('Autocomplete::UsersFinder::LIMIT', users_find_limit)
- context 'when user is an owner' do
- before do
- stub_const('Autocomplete::UsersFinder::LIMIT', users_find_limit)
+ sign_in(owner)
- sign_in(owner)
+ merge_request.assignees << assignee
- merge_request.assignees << assignee
+ visit project_merge_request_path(project, merge_request)
- visit project_merge_request_path(project, merge_request)
+ wait_for_requests
+ end
- wait_for_requests
+ shared_examples 'when assigned' do |expected_tooltip: '', expected_cannot_merge: ''|
+ it 'shows assignee name' do
+ expect(sidebar_assignee_block).to have_text(assignee.name)
end
- shared_examples 'when assigned' do |expected_tooltip: '', expected_cannot_merge: ''|
- it 'shows assignee name' do
- expect(sidebar_assignee_block).to have_text(assignee.name)
- end
+ it "sets data-cannot-merge to '#{expected_cannot_merge}'" do
+ expect(sidebar_assignee_merge_ability).to eql(expected_cannot_merge)
+ end
- it "sets data-cannot-merge to '#{expected_cannot_merge}'" do
- expect(sidebar_assignee_merge_ability).to eql(expected_cannot_merge)
+ context 'when edit is clicked' do
+ before do
+ open_assignees_dropdown
end
- context 'when edit is clicked' do
- before do
- sidebar_assignee_block.click_link('Edit')
-
- wait_for_requests
- end
-
- it "shows assignee tooltip '#{expected_tooltip}" do
- expect(sidebar_assignee_dropdown_tooltip).to eql(expected_tooltip)
- end
+ it "shows assignee tooltip '#{expected_tooltip}" do
+ expect(sidebar_assignee_dropdown_tooltip).to eql(expected_tooltip)
end
end
-
- context 'when assigned to maintainer' do
- let(:assignee) { project_maintainers.last }
-
- it_behaves_like 'when assigned', expected_tooltip: ''
- end
-
- context 'when assigned to developer' do
- let(:assignee) { project_developers.last }
-
- it_behaves_like 'when assigned', expected_tooltip: 'Cannot merge', expected_cannot_merge: 'true'
- end
end
- context 'with members shared into ancestors of the project' do
- before do
- sign_in(owner)
-
- visit project_merge_request_path(project, merge_request)
- wait_for_requests
+ context 'when assigned to maintainer' do
+ let(:assignee) { project_maintainers.last }
- sidebar_assignee_block.click_link('Edit')
- wait_for_requests
- end
-
- it 'contains the members shared into ancestors of the projects' do
- page.within '.dropdown-menu-user' do
- expect(page).to have_content shared_into_ancestor_user.name
- end
- end
+ it_behaves_like 'when assigned', expected_tooltip: ''
end
- context 'with invite members considerations' do
- let_it_be(:user) { create(:user) }
+ context 'when assigned to developer' do
+ let(:assignee) { project_developers.last }
- before do
- sign_in(user)
- end
-
- include_examples 'issuable invite members' do
- let(:issuable_path) { project_merge_request_path(project, merge_request) }
- end
+ it_behaves_like 'when assigned', expected_tooltip: 'Cannot merge', expected_cannot_merge: 'true'
end
end
- context 'when GraphQL assignees widget feature flag is enabled' do
- let(:sidebar_assignee_dropdown_item) { sidebar_assignee_block.find(".dropdown-item", text: assignee.username) }
- let(:sidebar_assignee_dropdown_tooltip) { sidebar_assignee_dropdown_item['title'] }
-
- context 'when user is an owner' do
- before do
- stub_const('Autocomplete::UsersFinder::LIMIT', users_find_limit)
-
- sign_in(owner)
-
- merge_request.assignees << assignee
-
- visit project_merge_request_path(project, merge_request)
-
- wait_for_requests
- end
-
- shared_examples 'when assigned' do |expected_tooltip: '', expected_cannot_merge: ''|
- it 'shows assignee name' do
- expect(sidebar_assignee_block).to have_text(assignee.name)
- end
-
- it "sets data-cannot-merge to '#{expected_cannot_merge}'" do
- expect(sidebar_assignee_merge_ability).to eql(expected_cannot_merge)
- end
-
- context 'when edit is clicked' do
- before do
- open_assignees_dropdown
- end
-
- it "shows assignee tooltip '#{expected_tooltip}" do
- expect(sidebar_assignee_dropdown_tooltip).to eql(expected_tooltip)
- end
- end
- end
-
- context 'when assigned to maintainer' do
- let(:assignee) { project_maintainers.last }
-
- it_behaves_like 'when assigned', expected_tooltip: ''
- end
+ context 'with members shared into ancestors of the project' do
+ before do
+ sign_in(owner)
- context 'when assigned to developer' do
- let(:assignee) { project_developers.last }
+ visit project_merge_request_path(project, merge_request)
+ wait_for_requests
- it_behaves_like 'when assigned', expected_tooltip: 'Cannot merge', expected_cannot_merge: 'true'
- end
+ open_assignees_dropdown
end
- context 'with members shared into ancestors of the project' do
- before do
- sign_in(owner)
-
- visit project_merge_request_path(project, merge_request)
- wait_for_requests
-
- open_assignees_dropdown
- end
-
- it 'contains the members shared into ancestors of the projects' do
- page.within '.dropdown-menu-user' do
- expect(page).to have_content shared_into_ancestor_user.name
- end
+ it 'contains the members shared into ancestors of the projects' do
+ page.within '.dropdown-menu-user' do
+ expect(page).to have_content shared_into_ancestor_user.name
end
end
+ end
- context 'with invite members considerations' do
- let_it_be(:user) { create(:user) }
-
- before do
- sign_in(user)
- end
-
- # TODO: Move to shared examples when feature flag is removed: https://gitlab.com/gitlab-org/gitlab/-/issues/328185
- context 'when a privileged user can invite' do
- it 'shows a link for inviting members and launches invite modal' do
- project.add_maintainer(user)
- visit project_merge_request_path(project, merge_request)
-
- open_assignees_dropdown
-
- page.within '.dropdown-menu-user' do
- expect(page).to have_link('Invite members')
-
- click_link 'Invite members'
- end
-
- page.within invite_modal_selector do
- expect(page).to have_content("You're inviting members to the #{project.name} project")
- end
- end
- end
-
- context 'when user cannot invite members in assignee dropdown' do
- it 'shows author in assignee dropdown and no invite link' do
- project.add_developer(user)
- visit project_merge_request_path(project, merge_request)
+ context 'with invite members considerations' do
+ let_it_be(:user) { create(:user) }
- open_assignees_dropdown
+ before do
+ sign_in(user)
+ end
- page.within '.dropdown-menu-user' do
- expect(page).not_to have_link('Invite members')
- end
- end
- end
+ include_examples 'issuable invite members' do
+ let(:issuable_path) { project_merge_request_path(project, merge_request) }
end
end
diff --git a/spec/features/merge_request/user_locks_discussion_spec.rb b/spec/features/merge_request/user_locks_discussion_spec.rb
index a603a5c1e0b..d4cc6c9410c 100644
--- a/spec/features/merge_request/user_locks_discussion_spec.rb
+++ b/spec/features/merge_request/user_locks_discussion_spec.rb
@@ -43,7 +43,7 @@ RSpec.describe 'Merge request > User locks discussion', :js, feature_category: :
page.within('.js-vue-notes-event') do
expect(page).not_to have_selector('js-main-target-form')
expect(page.find('.issuable-note-warning'))
- .to have_content('This merge request is locked. Only project members can comment.')
+ .to have_content('The discussion in this merge request is locked. Only project members can comment.')
end
end
end
diff --git a/spec/features/merge_request/user_merges_immediately_spec.rb b/spec/features/merge_request/user_merges_immediately_spec.rb
index 71af2045bab..ae229651579 100644
--- a/spec/features/merge_request/user_merges_immediately_spec.rb
+++ b/spec/features/merge_request/user_merges_immediately_spec.rb
@@ -37,7 +37,7 @@ RSpec.describe 'Merge requests > User merges immediately', :js, feature_category
wait_for_requests
page.within '[data-testid="ready_to_merge_state"]' do
- find('.dropdown-toggle').click
+ find('.gl-new-dropdown-toggle').click
Sidekiq::Testing.fake! do
click_button 'Merge immediately'
diff --git a/spec/features/merge_request/user_merges_merge_request_spec.rb b/spec/features/merge_request/user_merges_merge_request_spec.rb
index ede686cc700..111204a7105 100644
--- a/spec/features/merge_request/user_merges_merge_request_spec.rb
+++ b/spec/features/merge_request/user_merges_merge_request_spec.rb
@@ -3,7 +3,7 @@
require "spec_helper"
RSpec.describe "User merges a merge request", :js, feature_category: :code_review_workflow do
- let_it_be(:user) { create(:user, :no_super_sidebar) }
+ let_it_be(:user) { create(:user) }
before do
sign_in(user)
@@ -21,7 +21,8 @@ RSpec.describe "User merges a merge request", :js, feature_category: :code_revie
end
end
- context 'sidebar merge requests counter' do
+ # Pending re-implementation: https://gitlab.com/gitlab-org/gitlab/-/issues/429268
+ xcontext 'sidebar merge requests counter' do
let_it_be(:project) { create(:project, :public, :repository, namespace: user.namespace) }
let!(:merge_request) { create(:merge_request, source_project: project) }
diff --git a/spec/features/merge_request/user_opens_checkout_branch_modal_spec.rb b/spec/features/merge_request/user_opens_checkout_branch_modal_spec.rb
index 230111fe439..111e8574bac 100644
--- a/spec/features/merge_request/user_opens_checkout_branch_modal_spec.rb
+++ b/spec/features/merge_request/user_opens_checkout_branch_modal_spec.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe 'Merge request > User opens checkout branch modal', :js, feature_category: :code_review_workflow do
include ProjectForksHelper
- let_it_be(:user) { create(:user, :no_super_sidebar) }
+ let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project, :public, :repository, namespace: user.namespace) }
before do
@@ -13,7 +13,7 @@ RSpec.describe 'Merge request > User opens checkout branch modal', :js, feature_
end
describe 'for fork' do
- let(:author) { create(:user, :no_super_sidebar) }
+ let(:author) { create(:user) }
let(:source_project) { fork_project(project, author, repository: true) }
let(:merge_request) do
@@ -31,8 +31,10 @@ RSpec.describe 'Merge request > User opens checkout branch modal', :js, feature_
it 'shows instructions' do
visit project_merge_request_path(project, merge_request)
- click_button 'Code'
- click_button 'Check out branch'
+ page.within 'main' do
+ click_button 'Code'
+ click_button 'Check out branch'
+ end
expect(page).to have_content(source_project.http_url_to_repo)
end
diff --git a/spec/features/merge_request/user_reverts_merge_request_spec.rb b/spec/features/merge_request/user_reverts_merge_request_spec.rb
index 8c782056aa4..c2f82039f0b 100644
--- a/spec/features/merge_request/user_reverts_merge_request_spec.rb
+++ b/spec/features/merge_request/user_reverts_merge_request_spec.rb
@@ -3,23 +3,28 @@
require 'spec_helper'
RSpec.describe 'User reverts a merge request', :js, feature_category: :code_review_workflow do
+ include Spec::Support::Helpers::ModalHelpers
+
let(:merge_request) { create(:merge_request, :simple, source_project: project) }
let(:project) { create(:project, :public, :repository) }
let(:user) { create(:user) }
before do
- stub_feature_flags(unbatch_graphql_queries: false)
project.add_developer(user)
sign_in(user)
+ set_cookie('new-actions-popover-viewed', 'true')
visit(merge_request_path(merge_request))
page.within('.mr-state-widget') do
click_button 'Merge'
end
- wait_for_requests
+ wait_for_all_requests
+ page.refresh
+
+ wait_for_requests
# do not reload the page by visiting, let javascript update the page as it will validate we have loaded the modal
# code correctly on page update that adds the `revert` button
end
@@ -55,11 +60,11 @@ RSpec.describe 'User reverts a merge request', :js, feature_category: :code_revi
end
def revert_commit(create_merge_request: false)
- click_button('Revert')
+ click_button 'Revert'
- page.within('[data-testid="modal-commit"]') do
+ within_modal do
uncheck('create_merge_request') unless create_merge_request
- click_button('Revert')
+ click_button 'Revert'
end
end
end
diff --git a/spec/features/merge_request/user_sees_check_out_branch_modal_spec.rb b/spec/features/merge_request/user_sees_check_out_branch_modal_spec.rb
index 921c12134a9..da290f59736 100644
--- a/spec/features/merge_request/user_sees_check_out_branch_modal_spec.rb
+++ b/spec/features/merge_request/user_sees_check_out_branch_modal_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe 'Merge request > User sees check out branch modal', :js, feature_category: :code_review_workflow do
- let_it_be(:user) { create(:user, :no_super_sidebar) }
+ let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project, :public, :repository, creator: user) }
let_it_be(:merge_request) { create(:merge_request, source_project: project) }
let(:modal_window_title) { 'Check out, review, and resolve locally' }
@@ -13,8 +13,10 @@ RSpec.describe 'Merge request > User sees check out branch modal', :js, feature_
visit project_merge_request_path(project, merge_request)
wait_for_requests
- click_button 'Code'
- click_button('Check out branch')
+ page.within 'main' do
+ click_button 'Code'
+ click_button('Check out branch')
+ end
end
it 'shows the check out branch modal' do
diff --git a/spec/features/merge_request/user_sees_merge_request_file_tree_sidebar_spec.rb b/spec/features/merge_request/user_sees_merge_request_file_tree_sidebar_spec.rb
index c385def6762..8caa13c6297 100644
--- a/spec/features/merge_request/user_sees_merge_request_file_tree_sidebar_spec.rb
+++ b/spec/features/merge_request/user_sees_merge_request_file_tree_sidebar_spec.rb
@@ -15,48 +15,56 @@ RSpec.describe 'Merge request > User sees merge request file tree sidebar', :js,
sign_in(user)
visit diffs_project_merge_request_path(project, merge_request)
wait_for_requests
- scroll_into_view
end
it 'sees file tree sidebar' do
expect(page).to have_selector('.file-row[role=button]')
end
- # TODO: fix this test
- # For some reason the browser in CI doesn't update the file tree sidebar when review bar is shown
- # https://gitlab.com/gitlab-org/gitlab/-/merge_requests/118378#note_1403906356
- #
- # it 'has last entry visible with discussions enabled' do
- # add_diff_line_draft_comment('foo', find('.line_holder', match: :first))
- # scroll_into_view
- # scroll_to_end
- # button = find_all('.file-row[role=button]').last
- # expect(button.obscured?).to be_falsy
- # end
-
- shared_examples 'shows last visible file in sidebar' do
- it 'shows last file' do
- scroll_to_end
+ shared_examples 'last entry clickable' do
+ specify do
+ sidebar_scroller.execute_script('this.scrollBy(0,99999)')
button = find_all('.file-row[role=button]').last
title = button.find('[data-testid=file-row-name-container]')[:title]
+ expect(button.obscured?).to be_falsy
button.click
expect(page).to have_selector(".file-title-name[title*=\"#{title}\"]")
end
end
- it_behaves_like 'shows last visible file in sidebar'
+ it_behaves_like 'last entry clickable'
+
+ context 'when has started a review' do
+ before do
+ add_diff_line_draft_comment('foo', find('.line_holder', match: :first))
+ # wait for review bar to appear
+ find_by_testid('review_bar_component')
+ # wait for sidebar to adjust
+ sleep(1)
+ end
+
+ it_behaves_like 'last entry clickable'
+
+ context 'when scrolled into full view' do
+ before do
+ sidebar.execute_script("this.scrollIntoView({ block: 'end' })")
+ end
+
+ it_behaves_like 'last entry clickable'
+ end
+ end
context 'when viewing using file-by-file mode' do
let(:user) { create(:user, view_diffs_file_by_file: true) }
- it_behaves_like 'shows last visible file in sidebar'
- end
+ it_behaves_like 'last entry clickable'
- def scroll_into_view
- sidebar.execute_script("this.scrollIntoView({ block: 'end' })")
- end
+ context 'when navigating to the next file' do
+ before do
+ click_link 'Next'
+ end
- def scroll_to_end
- sidebar_scroller.execute_script('this.scrollBy(0,99999)')
+ it_behaves_like 'last entry clickable'
+ end
end
end
diff --git a/spec/features/merge_request/user_sees_merge_request_pipelines_spec.rb b/spec/features/merge_request/user_sees_merge_request_pipelines_spec.rb
index 69eb6b0dc17..5e683ddf7ba 100644
--- a/spec/features/merge_request/user_sees_merge_request_pipelines_spec.rb
+++ b/spec/features/merge_request/user_sees_merge_request_pipelines_spec.rb
@@ -86,7 +86,7 @@ RSpec.describe 'Merge request > User sees pipelines triggered by merge request',
it 'sees branch pipelines and detached merge request pipelines in correct order' do
page.within('.ci-table') do
- expect(page).to have_selector('[data-testid="ci-badge-link"]', text: 'Created', count: 2)
+ expect(page).to have_selector('[data-testid="ci-icon"]', text: 'Created', count: 2)
expect(first('[data-testid="pipeline-url-link"]')).to have_content("##{detached_merge_request_pipeline.id}")
end
end
@@ -122,7 +122,7 @@ RSpec.describe 'Merge request > User sees pipelines triggered by merge request',
it 'sees branch pipelines and detached merge request pipelines in correct order' do
page.within('.ci-table') do
- expect(page).to have_selector('[data-testid="ci-badge-link"]', text: 'Pending', count: 4)
+ expect(page).to have_selector('[data-testid="ci-icon"]', text: 'Pending', count: 4)
expect(all('[data-testid="pipeline-url-link"]')[0])
.to have_content("##{detached_merge_request_pipeline_2.id}")
@@ -220,7 +220,7 @@ RSpec.describe 'Merge request > User sees pipelines triggered by merge request',
it 'sees a branch pipeline in pipeline tab' do
page.within('.ci-table') do
- expect(page).to have_selector('[data-testid="ci-badge-link"]', text: 'Created', count: 1)
+ expect(page).to have_selector('[data-testid="ci-icon"]', text: 'Created', count: 1)
expect(first('[data-testid="pipeline-url-link"]')).to have_content("##{push_pipeline.id}")
end
end
@@ -273,7 +273,7 @@ RSpec.describe 'Merge request > User sees pipelines triggered by merge request',
it 'sees branch pipelines and detached merge request pipelines in correct order' do
page.within('.ci-table') do
- expect(page).to have_selector('[data-testid="ci-badge-link"]', text: 'Pending', count: 2)
+ expect(page).to have_selector('[data-testid="ci-icon"]', text: 'Pending', count: 2)
expect(first('[data-testid="pipeline-url-link"]')).to have_content("##{detached_merge_request_pipeline.id}")
end
end
@@ -289,7 +289,7 @@ RSpec.describe 'Merge request > User sees pipelines triggered by merge request',
it 'sees pipeline list in forked project' do
visit project_pipelines_path(forked_project)
- expect(page).to have_selector('[data-testid="ci-badge-link"]', text: 'Pending', count: 2)
+ expect(page).to have_selector('[data-testid="ci-icon"]', text: 'Pending', count: 2)
end
context 'when a user updated a merge request from a forked project to the parent project' do
@@ -315,7 +315,7 @@ RSpec.describe 'Merge request > User sees pipelines triggered by merge request',
it 'sees branch pipelines and detached merge request pipelines in correct order' do
page.within('.ci-table') do
- expect(page).to have_selector('[data-testid="ci-badge-link"]', text: 'Pending', count: 4)
+ expect(page).to have_selector('[data-testid="ci-icon"]', text: 'Pending', count: 4)
expect(all('[data-testid="pipeline-url-link"]')[0])
.to have_content("##{detached_merge_request_pipeline_2.id}")
@@ -358,7 +358,7 @@ RSpec.describe 'Merge request > User sees pipelines triggered by merge request',
it 'sees pipeline list in forked project' do
visit project_pipelines_path(forked_project)
- expect(page).to have_selector('[data-testid="ci-badge-link"]', text: 'Pending', count: 4)
+ expect(page).to have_selector('[data-testid="ci-icon"]', text: 'Pending', count: 4)
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 96cad397441..c18b2c97f96 100644
--- a/spec/features/merge_request/user_sees_merge_widget_spec.rb
+++ b/spec/features/merge_request/user_sees_merge_widget_spec.rb
@@ -53,7 +53,6 @@ RSpec.describe 'Merge request > User sees merge widget', :js, feature_category:
let!(:deployment) { build.deployment }
before do
- stub_feature_flags(unbatch_graphql_queries: false)
merge_request.update!(head_pipeline: pipeline)
deployment.update!(status: :success)
visit project_merge_request_path(project, merge_request)
@@ -84,6 +83,8 @@ RSpec.describe 'Merge request > User sees merge widget', :js, feature_category:
wait_for_requests
+ page.refresh
+
click_button 'Cherry-pick'
page.within(modal_selector) do
@@ -175,7 +176,7 @@ RSpec.describe 'Merge request > User sees merge widget', :js, feature_category:
expect(page).to have_content("Merge blocked")
expect(page).to have_content(
"pipeline must succeed. It's waiting for a manual action to continue.")
- expect(page).to have_css('.ci-status-icon-manual')
+ expect(page).to have_css('[data-testid="status_manual_borderless-icon"]')
end
end
diff --git a/spec/features/merge_request/user_sees_pipelines_spec.rb b/spec/features/merge_request/user_sees_pipelines_spec.rb
index a68b3c444fe..a06d1808b6b 100644
--- a/spec/features/merge_request/user_sees_pipelines_spec.rb
+++ b/spec/features/merge_request/user_sees_pipelines_spec.rb
@@ -49,9 +49,8 @@ RSpec.describe 'Merge request > User sees pipelines', :js, feature_category: :co
wait_for_requests
page.within(find('[data-testid="pipeline-table-row"]', match: :first)) do
- expect(page).to have_selector('[data-testid="ci-badge-link"]', text: 'Passed')
+ expect(page).to have_selector('[data-testid="ci-icon"]', text: 'Passed')
expect(page).to have_content(pipeline.id)
- expect(page).to have_content('API')
expect(page).to have_css('[data-testid="pipeline-mini-graph"]')
expect(page).to have_css('[data-testid="pipelines-manual-actions-dropdown"]')
expect(page).to have_css('[data-testid="pipeline-multi-actions-dropdown"]')
diff --git a/spec/features/merge_request/user_selects_branches_for_new_mr_spec.rb b/spec/features/merge_request/user_selects_branches_for_new_mr_spec.rb
index 654c71c87e0..daa84227adc 100644
--- a/spec/features/merge_request/user_selects_branches_for_new_mr_spec.rb
+++ b/spec/features/merge_request/user_selects_branches_for_new_mr_spec.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe 'Merge request > User selects branches for new MR', :js, feature_category: :code_review_workflow do
include ListboxHelpers
- let_it_be(:user) { create(:user, :no_super_sidebar) }
+ let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project, :public, :repository, namespace: user.namespace) }
def select_source_branch(branch_name)
@@ -64,8 +64,10 @@ RSpec.describe 'Merge request > User selects branches for new MR', :js, feature_
fill_in "merge_request_title", with: "Orphaned MR test"
click_button "Create merge request"
- click_button 'Code'
- click_button "Check out branch"
+ page.within 'main' do
+ click_button 'Code'
+ click_button "Check out branch"
+ end
expect(page).to have_content 'git checkout -b \'orphaned-branch\' \'origin/orphaned-branch\''
end
diff --git a/spec/features/merge_request/user_uses_quick_actions_spec.rb b/spec/features/merge_request/user_uses_quick_actions_spec.rb
index b2cc25f1c34..a89f533c9dd 100644
--- a/spec/features/merge_request/user_uses_quick_actions_spec.rb
+++ b/spec/features/merge_request/user_uses_quick_actions_spec.rb
@@ -13,7 +13,7 @@ RSpec.describe 'Merge request > User uses quick actions', :js, :use_clean_rails_
context "issuable common quick actions" do
let!(:new_url_opts) { { merge_request: { source_branch: 'feature', target_branch: 'master' } } }
- let(:maintainer) { create(:user, :no_super_sidebar) }
+ let(:maintainer) { create(:user) }
let(:project) { create(:project, :public, :repository) }
let!(:label_bug) { create(:label, project: project, title: 'bug') }
let!(:label_feature) { create(:label, project: project, title: 'feature') }
@@ -26,8 +26,8 @@ RSpec.describe 'Merge request > User uses quick actions', :js, :use_clean_rails_
end
describe 'merge-request-only commands' do
- let(:user) { create(:user, :no_super_sidebar) }
- let(:guest) { create(:user, :no_super_sidebar) }
+ let(:user) { create(:user) }
+ let(:guest) { create(:user) }
let(:project) { create(:project, :public, :repository) }
let(:merge_request) { create(:merge_request, source_project: project) }
let!(:milestone) { create(:milestone, project: project, title: 'ASAP') }
diff --git a/spec/features/merge_requests/user_filters_by_source_branch_spec.rb b/spec/features/merge_requests/user_filters_by_source_branch_spec.rb
new file mode 100644
index 00000000000..7eade94de2a
--- /dev/null
+++ b/spec/features/merge_requests/user_filters_by_source_branch_spec.rb
@@ -0,0 +1,65 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Merge Requests > User filters by source branch', :js, feature_category: :code_review_workflow do
+ include FilteredSearchHelpers
+
+ def create_mr(source_branch, target_branch, status)
+ create(:merge_request, status, source_project: project,
+ target_branch: target_branch, source_branch: source_branch)
+ end
+
+ let_it_be(:project) { create(:project, :public, :repository) }
+ let_it_be(:user) { project.creator }
+
+ let_it_be(:mr1) { create_mr('source1', 'target1', :opened) }
+ let_it_be(:mr2) { create_mr('source2', 'target1', :opened) }
+ let_it_be(:mr3) { create_mr('source1', 'target2', :merged) }
+ let_it_be(:mr4) { create_mr('source1', 'target2', :closed) }
+
+ before do
+ sign_in(user)
+ visit project_merge_requests_path(project)
+ end
+
+ context 'when filtering by source-branch:source1' do
+ it 'applies the filter' do
+ input_filtered_search('source-branch:=source1')
+
+ expect(page).to have_issuable_counts(open: 1, merged: 1, closed: 1, all: 3)
+ expect(page).to have_content mr1.title
+ expect(page).not_to have_content mr2.title
+ end
+ end
+
+ context 'when filtering by source-branch:source2' do
+ it 'applies the filter' do
+ input_filtered_search('source-branch:=source2')
+
+ expect(page).to have_issuable_counts(open: 1, merged: 0, closed: 0, all: 1)
+ expect(page).not_to have_content mr1.title
+ expect(page).to have_content mr2.title
+ end
+ end
+
+ context 'when filtering by source-branch:non-exists-branch' do
+ it 'applies the filter' do
+ input_filtered_search('source-branch:=non-exists-branch')
+
+ expect(page).to have_issuable_counts(open: 0, merged: 0, closed: 0, all: 0)
+ expect(page).not_to have_content mr1.title
+ expect(page).not_to have_content mr2.title
+ end
+ end
+
+ context 'when filtering by source-branch:!=source1' do
+ it 'applies the filter' do
+ input_filtered_search('source-branch:!=source1')
+
+ expect(page).to have_issuable_counts(open: 1, merged: 0, closed: 0, all: 1)
+ expect(page).not_to have_content mr1.title
+ expect(page).to have_content mr2.title
+ end
+ end
+end
diff --git a/spec/features/monitor_sidebar_link_spec.rb b/spec/features/monitor_sidebar_link_spec.rb
index 1d39f749ca7..1855379825b 100644
--- a/spec/features/monitor_sidebar_link_spec.rb
+++ b/spec/features/monitor_sidebar_link_spec.rb
@@ -2,9 +2,9 @@
require 'spec_helper'
-RSpec.describe 'Monitor dropdown sidebar', :aggregate_failures, feature_category: :shared do
+RSpec.describe 'Monitor dropdown sidebar', :js, feature_category: :shared do
let_it_be_with_reload(:project) { create(:project, :internal, :repository) }
- let_it_be(:user) { create(:user, :no_super_sidebar) }
+ let_it_be(:user) { create(:user) }
let(:role) { nil }
@@ -13,7 +13,7 @@ RSpec.describe 'Monitor dropdown sidebar', :aggregate_failures, feature_category
sign_in(user)
end
- shared_examples 'shows Monitor menu based on the access level' do
+ shared_examples 'shows common Monitor menu item based on the access level' do
using RSpec::Parameterized::TableSyntax
let(:enabled) { Featurable::PRIVATE }
@@ -30,10 +30,14 @@ RSpec.describe 'Monitor dropdown sidebar', :aggregate_failures, feature_category
visit project_issues_path(project)
- if render
- expect(page).to have_selector('a.shortcuts-monitor', text: 'Monitor')
- else
- expect(page).not_to have_selector('a.shortcuts-monitor')
+ click_button('Monitor')
+
+ within_testid('super-sidebar') do
+ if render
+ expect(page).to have_link('Incidents')
+ else
+ expect(page).not_to have_link('Incidents', visible: :all)
+ end
end
end
end
@@ -44,32 +48,35 @@ RSpec.describe 'Monitor dropdown sidebar', :aggregate_failures, feature_category
before do
project.project_feature.update_attribute(:monitor_access_level, access_level)
+ visit project_issues_path(project)
+ click_button('Monitor')
end
- it 'has the correct `Monitor` menu items', :aggregate_failures do
- visit project_issues_path(project)
- expect(page).to have_selector('a.shortcuts-monitor', text: 'Monitor')
+ it 'has the correct `Monitor` and `Operate` menu items' do
expect(page).to have_link('Incidents', href: project_incidents_path(project))
+
+ click_button('Operate')
+
expect(page).to have_link('Environments', href: project_environments_path(project))
- expect(page).not_to have_link('Alerts', href: project_alert_management_index_path(project))
- expect(page).not_to have_link('Error Tracking', href: project_error_tracking_index_path(project))
- expect(page).not_to have_link('Kubernetes', href: project_clusters_path(project))
+ expect(page).not_to have_link('Alerts', href: project_alert_management_index_path(project), visible: :all)
+ expect(page).not_to have_link('Error Tracking', href: project_error_tracking_index_path(project), visible: :all)
+ expect(page).not_to have_link('Kubernetes clusters', href: project_clusters_path(project), visible: :all)
end
context 'when monitor project feature is PRIVATE' do
let(:access_level) { ProjectFeature::PRIVATE }
- it 'does not show the `Monitor` menu' do
- expect(page).not_to have_selector('a.shortcuts-monitor')
+ it 'does not show common items of the `Monitor` menu' do
+ expect(page).not_to have_link('Error Tracking', href: project_incidents_path(project), visible: :all)
end
end
context 'when monitor project feature is DISABLED' do
let(:access_level) { ProjectFeature::DISABLED }
- it 'does not show the `Monitor` menu' do
- expect(page).not_to have_selector('a.shortcuts-monitor')
+ it 'does not show the `Incidents` menu' do
+ expect(page).not_to have_link('Error Tracking', href: project_incidents_path(project), visible: :all)
end
end
end
@@ -77,63 +84,86 @@ RSpec.describe 'Monitor dropdown sidebar', :aggregate_failures, feature_category
context 'when user has guest role' do
let(:role) { :guest }
- it 'has the correct `Monitor` menu items' do
+ it 'has the correct `Monitor` and `Operate` menu items' do
visit project_issues_path(project)
- expect(page).to have_selector('a.shortcuts-monitor', text: 'Monitor')
+
+ click_button('Monitor')
+
expect(page).to have_link('Incidents', href: project_incidents_path(project))
+
+ click_button('Operate')
+
expect(page).to have_link('Environments', href: project_environments_path(project))
- expect(page).not_to have_link('Alerts', href: project_alert_management_index_path(project))
- expect(page).not_to have_link('Error Tracking', href: project_error_tracking_index_path(project))
- expect(page).not_to have_link('Kubernetes', href: project_clusters_path(project))
+ expect(page).not_to have_link('Alerts', href: project_alert_management_index_path(project), visible: :all)
+ expect(page).not_to have_link('Error Tracking', href: project_error_tracking_index_path(project), visible: :all)
+ expect(page).not_to have_link('Kubernetes clusters', href: project_clusters_path(project), visible: :all)
end
- it_behaves_like 'shows Monitor menu based on the access level'
+ it_behaves_like 'shows common Monitor menu item based on the access level'
end
context 'when user has reporter role' do
let(:role) { :reporter }
- it 'has the correct `Monitor` menu items' do
+ it 'has the correct `Monitor` and `Operate` menu items' do
visit project_issues_path(project)
+
+ click_button('Monitor')
+
expect(page).to have_link('Incidents', href: project_incidents_path(project))
- expect(page).to have_link('Environments', href: project_environments_path(project))
expect(page).to have_link('Error Tracking', href: project_error_tracking_index_path(project))
- expect(page).not_to have_link('Alerts', href: project_alert_management_index_path(project))
- expect(page).not_to have_link('Kubernetes', href: project_clusters_path(project))
+ click_button('Operate')
+
+ expect(page).to have_link('Environments', href: project_environments_path(project))
+
+ expect(page).not_to have_link('Alerts', href: project_alert_management_index_path(project), visible: :all)
+ expect(page).not_to have_link('Kubernetes clusters', href: project_clusters_path(project), visible: :all)
end
- it_behaves_like 'shows Monitor menu based on the access level'
+ it_behaves_like 'shows common Monitor menu item based on the access level'
end
context 'when user has developer role' do
let(:role) { :developer }
- it 'has the correct `Monitor` menu items' do
+ it 'has the correct `Monitor` and `Operate` menu items' do
visit project_issues_path(project)
+
+ click_button('Monitor')
+
expect(page).to have_link('Alerts', href: project_alert_management_index_path(project))
expect(page).to have_link('Incidents', href: project_incidents_path(project))
- expect(page).to have_link('Environments', href: project_environments_path(project))
expect(page).to have_link('Error Tracking', href: project_error_tracking_index_path(project))
- expect(page).to have_link('Kubernetes', href: project_clusters_path(project))
+
+ click_button('Operate')
+
+ expect(page).to have_link('Environments', href: project_environments_path(project))
+ expect(page).to have_link('Kubernetes clusters', href: project_clusters_path(project))
end
- it_behaves_like 'shows Monitor menu based on the access level'
+ it_behaves_like 'shows common Monitor menu item based on the access level'
end
context 'when user has maintainer role' do
let(:role) { :maintainer }
- it 'has the correct `Monitor` menu items' do
+ it 'has the correct `Monitor` and `Operate` menu items' do
visit project_issues_path(project)
+
+ click_button('Monitor')
+
expect(page).to have_link('Alerts', href: project_alert_management_index_path(project))
expect(page).to have_link('Incidents', href: project_incidents_path(project))
- expect(page).to have_link('Environments', href: project_environments_path(project))
expect(page).to have_link('Error Tracking', href: project_error_tracking_index_path(project))
- expect(page).to have_link('Kubernetes', href: project_clusters_path(project))
+
+ click_button('Operate')
+
+ expect(page).to have_link('Environments', href: project_environments_path(project))
+ expect(page).to have_link('Kubernetes clusters', href: project_clusters_path(project))
end
- it_behaves_like 'shows Monitor menu based on the access level'
+ it_behaves_like 'shows common Monitor menu item based on the access level'
end
end
diff --git a/spec/features/nav/new_nav_callout_spec.rb b/spec/features/nav/new_nav_callout_spec.rb
deleted file mode 100644
index 22e7fd6b9f9..00000000000
--- a/spec/features/nav/new_nav_callout_spec.rb
+++ /dev/null
@@ -1,64 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe 'new navigation callout', :js, feature_category: :navigation do
- let_it_be(:callout_title) { _('Welcome to a new navigation experience') }
- let(:dot_com) { false }
-
- before do
- allow(Gitlab).to receive(:com?).and_return(dot_com)
- sign_in(user)
- visit root_path
- end
-
- context 'with new navigation toggled on' do
- let_it_be(:user) { create(:user, created_at: Date.new(2023, 6, 1), use_new_navigation: true) }
-
- it 'shows a callout about the new navigation' do
- expect(page).to have_content callout_title
- end
-
- context 'when user dismisses callout' do
- it 'hides callout' do
- expect(page).to have_content callout_title
-
- page.within(find('[data-feature-id="new_navigation_callout"]')) do
- find('[data-testid="close-icon"]').click
- end
-
- wait_for_requests
-
- visit root_path
-
- expect(page).not_to have_content callout_title
- end
- end
- end
-
- context 'when user registered on or after June 2nd 2023' do
- let_it_be(:user) { create(:user, created_at: Date.new(2023, 6, 2), use_new_navigation: true) }
-
- context 'when on GitLab.com' do
- let(:dot_com) { true }
-
- it 'does not show the callout about the new navigation' do
- expect(page).not_to have_content callout_title
- end
- end
-
- context 'when on a self-managed instance' do
- it 'shows the callout about the new navigation' do
- expect(page).to have_content callout_title
- end
- end
- end
-
- context 'with new navigation toggled off' do
- let_it_be(:user) { create(:user, created_at: Date.new(2023, 6, 1), use_new_navigation: false) }
-
- it 'does not show the callout' do
- expect(page).not_to have_content callout_title
- end
- end
-end
diff --git a/spec/features/nav/new_nav_for_everyone_callout_spec.rb b/spec/features/nav/new_nav_for_everyone_callout_spec.rb
new file mode 100644
index 00000000000..ad0b57298d7
--- /dev/null
+++ b/spec/features/nav/new_nav_for_everyone_callout_spec.rb
@@ -0,0 +1,55 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'new navigation for everyone callout', :js, feature_category: :navigation do
+ let_it_be(:callout_title) { _('GitLab has redesigned the left sidebar to address customer feedback') }
+
+ before do
+ sign_in(user)
+ visit root_path
+ end
+
+ context 'with new navigation previously toggled on' do
+ let_it_be(:user) { create(:user, use_new_navigation: true) }
+
+ it 'does not show the callout' do
+ expect(page).to have_css('[data-testid="super-sidebar"]')
+ expect(page).not_to have_content callout_title
+ end
+ end
+
+ context 'with new navigation previously toggled off' do
+ let_it_be(:user) { create(:user, use_new_navigation: false) }
+
+ it 'shows a callout about the new navigation now being active for everyone' do
+ expect(page).to have_css('[data-testid="super-sidebar"]')
+ expect(page).to have_content callout_title
+ end
+
+ context 'when user dismisses callout' do
+ it 'hides callout' do
+ expect(page).to have_content callout_title
+
+ page.within(find('[data-feature-id="new_nav_for_everyone_callout"]')) do
+ find_by_testid('close-icon').click
+ end
+
+ wait_for_requests
+
+ visit root_path
+
+ expect(page).not_to have_content callout_title
+ end
+ end
+ end
+
+ context 'with new navigation never toggled on or off' do
+ let_it_be(:user) { create(:user, use_new_navigation: nil) }
+
+ it 'does not show the callout' do
+ expect(page).to have_css('[data-testid="super-sidebar"]')
+ expect(page).not_to have_content callout_title
+ end
+ end
+end
diff --git a/spec/features/nav/new_nav_invite_members_spec.rb b/spec/features/nav/new_nav_invite_members_spec.rb
index 4c37d6b4760..7501745ec55 100644
--- a/spec/features/nav/new_nav_invite_members_spec.rb
+++ b/spec/features/nav/new_nav_invite_members_spec.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe 'new navigation toggle', :js, feature_category: :navigation do
include Features::InviteMembersModalHelpers
- let_it_be(:user) { create(:user, use_new_navigation: true) }
+ let_it_be(:user) { create(:user) }
before do
sign_in(user)
diff --git a/spec/features/nav/new_nav_toggle_spec.rb b/spec/features/nav/new_nav_toggle_spec.rb
deleted file mode 100644
index 6872058be8e..00000000000
--- a/spec/features/nav/new_nav_toggle_spec.rb
+++ /dev/null
@@ -1,59 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe 'new navigation toggle', :js, feature_category: :navigation do
- let_it_be(:user) { create(:user) }
-
- before do
- user.update!(use_new_navigation: user_preference)
- sign_in(user)
- visit explore_projects_path
- end
-
- context 'when user has new nav disabled' do
- let(:user_preference) { false }
-
- it 'allows to enable new nav', :aggregate_failures do
- within '.js-nav-user-dropdown' do
- find('a[data-toggle="dropdown"]').click
- expect(page).to have_content('Navigation redesign')
-
- toggle = page.find('.gl-toggle:not(.is-checked)')
- toggle.click # reloads the page
- end
-
- wait_for_requests
-
- expect(user.reload.use_new_navigation).to eq true
- end
-
- it 'shows the old navigation' do
- expect(page).to have_selector('.js-navbar')
- expect(page).not_to have_selector('[data-testid="super-sidebar"]')
- end
- end
-
- context 'when user has new nav enabled' do
- let(:user_preference) { true }
-
- it 'allows to disable new nav', :aggregate_failures do
- within '[data-testid="super-sidebar"] [data-testid="user-dropdown"]' do
- click_button "#{user.name} user’s menu"
- expect(page).to have_content('Navigation redesign')
-
- toggle = page.find('.gl-toggle.is-checked')
- toggle.click # reloads the page
- end
-
- wait_for_requests
-
- expect(user.reload.use_new_navigation).to eq false
- end
-
- it 'shows the new navigation' do
- expect(page).not_to have_selector('.js-navbar')
- expect(page).to have_selector('[data-testid="super-sidebar"]')
- end
- end
-end
diff --git a/spec/features/nav/pinned_nav_items_spec.rb b/spec/features/nav/pinned_nav_items_spec.rb
index b4d6464ec50..a2428048a1a 100644
--- a/spec/features/nav/pinned_nav_items_spec.rb
+++ b/spec/features/nav/pinned_nav_items_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe 'Navigation menu item pinning', :js, feature_category: :navigation do
- let_it_be(:user) { create(:user, use_new_navigation: true) }
+ let_it_be(:user) { create(:user) }
before do
sign_in(user)
diff --git a/spec/features/nav/top_nav_responsive_spec.rb b/spec/features/nav/top_nav_responsive_spec.rb
deleted file mode 100644
index 2a07742c91e..00000000000
--- a/spec/features/nav/top_nav_responsive_spec.rb
+++ /dev/null
@@ -1,101 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe 'top nav responsive', :js, feature_category: :navigation do
- include MobileHelpers
- include Features::InviteMembersModalHelpers
-
- let_it_be(:user) { create(:user, :no_super_sidebar) }
-
- before do
- sign_in(user)
-
- resize_screen_xs
- end
-
- context 'when outside groups and projects' do
- before do
- visit explore_projects_path
- end
-
- context 'when menu is closed' do
- it 'has page content and hides responsive menu', :aggregate_failures do
- expect(page).to have_css('.page-title', text: 'Explore projects')
- expect(page).to have_link('Homepage', id: 'logo')
-
- expect(page).to have_no_css('.top-nav-responsive')
- end
- end
-
- context 'when menu is opened' do
- before do
- click_button('Menu')
- end
-
- it 'hides everything and shows responsive menu', :aggregate_failures do
- expect(page).to have_no_css('.page-title', text: 'Explore projects')
- expect(page).to have_no_link('Homepage', id: 'logo')
-
- within '.top-nav-responsive' do
- expect(page).to have_link(nil, href: search_path)
- expect(page).to have_button('Projects')
- expect(page).to have_button('Groups')
- expect(page).to have_link('Your work', href: dashboard_projects_path)
- expect(page).to have_link('Explore', href: explore_projects_path)
- end
- end
-
- it 'has new dropdown', :aggregate_failures do
- create_new_button.click
-
- expect(page).to have_link('New project', href: new_project_path)
- expect(page).to have_link('New group', href: new_group_path)
- expect(page).to have_link('New snippet', href: new_snippet_path)
- end
- end
- end
-
- context 'when inside a project' do
- let_it_be(:project) { create(:project).tap { |record| record.add_owner(user) } }
-
- before do
- visit project_path(project)
- end
-
- it 'the add menu contains invite members dropdown option and opens invite modal' do
- invite_members_from_menu
-
- page.within invite_modal_selector do
- expect(page).to have_content("You're inviting members to the #{project.name} project")
- end
- end
- end
-
- context 'when inside a group' do
- let_it_be(:group) { create(:group).tap { |record| record.add_owner(user) } }
-
- before do
- visit group_path(group)
- end
-
- it 'the add menu contains invite members dropdown option and opens invite modal' do
- invite_members_from_menu
-
- page.within invite_modal_selector do
- expect(page).to have_content("You're inviting members to the #{group.name} group")
- end
- end
- end
-
- def invite_members_from_menu
- click_button('Menu')
- create_new_button.click
-
- click_button('Invite members')
- end
-
- def create_new_button
- find('[data-testid="plus-icon"]')
- end
-end
diff --git a/spec/features/nav/top_nav_spec.rb b/spec/features/nav/top_nav_spec.rb
deleted file mode 100644
index bf91897eb26..00000000000
--- a/spec/features/nav/top_nav_spec.rb
+++ /dev/null
@@ -1,51 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe 'top nav responsive', :js, feature_category: :navigation do
- include Features::InviteMembersModalHelpers
-
- let_it_be(:user) { create(:user, :no_super_sidebar) }
-
- before do
- sign_in(user)
- end
-
- context 'when inside a project' do
- let_it_be(:project) { create(:project).tap { |record| record.add_owner(user) } }
-
- before do
- visit project_path(project)
- end
-
- it 'the add menu contains invite members dropdown option and opens invite modal' do
- invite_members_from_menu
-
- page.within invite_modal_selector do
- expect(page).to have_content("You're inviting members to the #{project.name} project")
- end
- end
- end
-
- context 'when inside a group' do
- let_it_be(:group) { create(:group).tap { |record| record.add_owner(user) } }
-
- before do
- visit group_path(group)
- end
-
- it 'the add menu contains invite members dropdown option and opens invite modal' do
- invite_members_from_menu
-
- page.within invite_modal_selector do
- expect(page).to have_content("You're inviting members to the #{group.name} group")
- end
- end
- end
-
- def invite_members_from_menu
- find('[data-testid="new-menu-toggle"]').click
-
- click_link('Invite members')
- end
-end
diff --git a/spec/features/nav/top_nav_tooltip_spec.rb b/spec/features/nav/top_nav_tooltip_spec.rb
deleted file mode 100644
index 1afd1981a86..00000000000
--- a/spec/features/nav/top_nav_tooltip_spec.rb
+++ /dev/null
@@ -1,25 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe 'top nav tooltips', :js, feature_category: :navigation do
- let_it_be(:user) { create(:user) }
-
- before do
- sign_in(user)
- visit explore_projects_path
- end
-
- it 'clicking new dropdown hides tooltip', :aggregate_failures,
- quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/382786' do
- btn = '#js-onboarding-new-project-link'
-
- page.find(btn).hover
-
- expect(page).to have_content('Create new...')
-
- page.find(btn).click
-
- expect(page).not_to have_content('Create new...')
- end
-end
diff --git a/spec/features/profiles/two_factor_auths_spec.rb b/spec/features/profiles/two_factor_auths_spec.rb
index b52f66cfcee..15ab79684d9 100644
--- a/spec/features/profiles/two_factor_auths_spec.rb
+++ b/spec/features/profiles/two_factor_auths_spec.rb
@@ -59,7 +59,7 @@ RSpec.describe 'Two factor auths', feature_category: :user_profile do
fill_in 'pin_code', with: '123'
click_button 'Register with two-factor app'
- expect(page).to have_link('Try the troubleshooting steps here.', href: help_page_path('user/profile/account/two_factor_authentication.md', anchor: 'troubleshooting'))
+ expect(page).to have_link('Try the troubleshooting steps here.', href: help_page_path('user/profile/account/two_factor_authentication', anchor: 'troubleshooting'))
end
end
diff --git a/spec/features/profiles/user_edit_profile_spec.rb b/spec/features/profiles/user_edit_profile_spec.rb
index 697ad4c87f7..439839cfad5 100644
--- a/spec/features/profiles/user_edit_profile_spec.rb
+++ b/spec/features/profiles/user_edit_profile_spec.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe 'User edit profile', feature_category: :user_profile do
include Features::NotesHelpers
- let_it_be(:user) { create(:user, :no_super_sidebar) }
+ let_it_be_with_reload(:user) { create(:user) }
before do
stub_feature_flags(edit_user_profile_vue: false)
@@ -18,17 +18,6 @@ RSpec.describe 'User edit profile', feature_category: :user_profile do
wait_for_requests if respond_to?(:wait_for_requests)
end
- def update_user_email
- fill_in 'user_email', with: 'new-email@example.com'
- click_button 'Update profile settings'
- end
-
- def confirm_password(password)
- fill_in 'password-confirmation', with: password
- click_button 'Confirm password'
- wait_for_requests if respond_to?(:wait_for_requests)
- end
-
def visit_user
visit user_path(user)
wait_for_requests
@@ -119,6 +108,18 @@ RSpec.describe 'User edit profile', feature_category: :user_profile do
end
describe 'when I change my email', :js do
+ def update_user_email
+ fill_in 'user_email', with: '' # Clearing the email field
+ fill_in 'user_email', with: 'new-email@example.com'
+ submit_settings
+ end
+
+ def confirm_password(password)
+ fill_in 'password-confirmation', with: password
+ click_button 'Confirm password'
+ wait_for_requests if respond_to?(:wait_for_requests)
+ end
+
before do
user.send_reset_password_instructions
end
@@ -194,13 +195,20 @@ RSpec.describe 'User edit profile', feature_category: :user_profile do
emoji_button.click
end
+ after do
+ if user.status
+ user.status.destroy!
+ user.reload_status
+ end
+ end
+
context 'profile edit form' do
it 'shows the user status form' do
expect(page).to have_content('Current status')
end
it 'adds emoji to user status' do
- emoji = 'basketball'
+ emoji = 'laughing'
select_emoji(emoji)
submit_settings
@@ -225,7 +233,7 @@ RSpec.describe 'User edit profile', feature_category: :user_profile do
end
it 'adds message and emoji to user status' do
- emoji = '8ball'
+ emoji = 'grinning'
message = 'Playing outside'
select_emoji(emoji)
fill_in s_("SetStatusModal|What's your status?"), with: message
@@ -319,9 +327,9 @@ RSpec.describe 'User edit profile', feature_category: :user_profile do
let(:project) { create(:project) }
def open_modal(button_text)
- find('.header-user-dropdown-toggle').click
+ find_by_testid('user-dropdown').click
- page.within ".header-user" do
+ within_testid('user-dropdown') do
find('.js-set-status-modal-trigger.ready')
click_button button_text
@@ -348,9 +356,9 @@ RSpec.describe 'User edit profile', feature_category: :user_profile do
end
it 'shows the "Set status" menu item in the user menu' do
- find('.header-user-dropdown-toggle').click
+ find_by_testid('user-dropdown').click
- page.within ".header-user" do
+ within_testid('user-dropdown') do
expect(page).to have_content('Set status')
end
end
@@ -359,9 +367,9 @@ RSpec.describe 'User edit profile', feature_category: :user_profile do
user_status = create(:user_status, user: user, message: 'Eating bread', emoji: 'stuffed_flatbread')
visit root_path(user)
- find('.header-user-dropdown-toggle').click
+ find_by_testid('user-dropdown').click
- page.within ".header-user" do
+ within_testid('user-dropdown') do
expect(page).to have_emoji(user_status.emoji)
expect(page).to have_content user_status.message
expect(page).to have_content('Edit status')
@@ -376,7 +384,7 @@ RSpec.describe 'User edit profile', feature_category: :user_profile do
end
it 'adds emoji to user status' do
- emoji = '8ball'
+ emoji = 'grinning'
open_user_status_modal
select_emoji(emoji, true)
set_user_status_in_modal
@@ -407,7 +415,7 @@ RSpec.describe 'User edit profile', feature_category: :user_profile do
it 'opens the emoji modal again after closing it' do
open_user_status_modal
- select_emoji('8ball', true)
+ select_emoji('grinning', true)
find('.emoji-menu-toggle-button').click
@@ -418,7 +426,7 @@ RSpec.describe 'User edit profile', feature_category: :user_profile do
project.add_maintainer(user)
visit(project_issue_path(project, issue))
- emoji = '8ball'
+ emoji = 'grinning'
open_user_status_modal
select_emoji(emoji, true)
@@ -440,7 +448,7 @@ RSpec.describe 'User edit profile', feature_category: :user_profile do
end
it 'adds message and emoji to user status' do
- emoji = '8ball'
+ emoji = 'grinning'
message = 'Playing outside'
open_user_status_modal
select_emoji(emoji, true)
@@ -478,8 +486,6 @@ RSpec.describe 'User edit profile', feature_category: :user_profile do
end
context 'Remove status button' do
- let(:user) { create(:user, :no_super_sidebar) }
-
before do
user.status = UserStatus.new(message: 'Eating bread', emoji: 'stuffed_flatbread')
@@ -504,9 +510,9 @@ RSpec.describe 'User edit profile', feature_category: :user_profile do
it 'shows the "Set status" menu item in the user menu' do
visit root_path(user)
- find('.header-user-dropdown-toggle').click
+ find_by_testid('user-dropdown').click
- page.within ".header-user" do
+ within_testid('user-dropdown') do
expect(page).to have_content('Set status')
end
end
@@ -520,30 +526,30 @@ RSpec.describe 'User edit profile', feature_category: :user_profile do
expect(page).to have_emoji('speech_balloon')
end
end
+ end
- context 'User time preferences', :js do
- let(:issue) { create(:issue, project: project) }
- let(:project) { create(:project) }
+ context 'User time preferences', :js do
+ let(:issue) { create(:issue, project: project) }
+ let(:project) { create(:project) }
- it 'shows the user time preferences form' do
- expect(page).to have_content('Time settings')
- end
+ it 'shows the user time preferences form' do
+ expect(page).to have_content('Time settings')
+ end
- it 'allows the user to select a time zone from a dropdown list of options' do
- expect(page).not_to have_selector('.user-time-preferences [data-testid="base-dropdown-menu"]')
+ it 'allows the user to select a time zone from a dropdown list of options' do
+ expect(page).not_to have_selector('.user-time-preferences [data-testid="base-dropdown-menu"]')
- page.find('.user-time-preferences .gl-new-dropdown-toggle').click
+ page.find('.user-time-preferences .gl-new-dropdown-toggle').click
- expect(page.find('.user-time-preferences [data-testid="base-dropdown-menu"]')).to be_visible
+ expect(page.find('.user-time-preferences [data-testid="base-dropdown-menu"]')).to be_visible
- page.find("li", text: "Arizona").click
+ page.find("li", text: "Arizona").click
- expect(page).to have_field(:user_timezone, with: 'America/Phoenix', type: :hidden)
- end
+ expect(page).to have_field(:user_timezone, with: 'America/Phoenix', type: :hidden)
+ end
- it 'timezone defaults to empty' do
- expect(page).to have_field(:user_timezone, with: '', type: :hidden)
- end
+ it 'timezone defaults to empty' do
+ expect(page).to have_field(:user_timezone, with: '', type: :hidden)
end
end
diff --git a/spec/features/profiles/user_visits_profile_account_page_spec.rb b/spec/features/profiles/user_visits_profile_account_page_spec.rb
deleted file mode 100644
index 8569cefd1f4..00000000000
--- a/spec/features/profiles/user_visits_profile_account_page_spec.rb
+++ /dev/null
@@ -1,17 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe 'User visits the profile account page', feature_category: :user_profile do
- let(:user) { create(:user, :no_super_sidebar) }
-
- before do
- sign_in(user)
-
- visit(profile_account_path)
- end
-
- it 'shows correct menu item' do
- expect(page).to have_active_navigation('Account')
- end
-end
diff --git a/spec/features/profiles/user_visits_profile_authentication_log_spec.rb b/spec/features/profiles/user_visits_profile_authentication_log_spec.rb
index f92b8e2e751..c081f4a3ec9 100644
--- a/spec/features/profiles/user_visits_profile_authentication_log_spec.rb
+++ b/spec/features/profiles/user_visits_profile_authentication_log_spec.rb
@@ -3,19 +3,7 @@
require 'spec_helper'
RSpec.describe 'User visits the authentication log', feature_category: :user_profile do
- let(:user) { create(:user, :no_super_sidebar) }
-
- context 'when user signed in' do
- before do
- sign_in(user)
- end
-
- it 'shows correct menu item' do
- visit(audit_log_profile_path)
-
- expect(page).to have_active_navigation('Authentication Log')
- end
- end
+ let(:user) { create(:user) }
context 'when user has activity' do
before do
diff --git a/spec/features/profiles/user_visits_profile_preferences_page_spec.rb b/spec/features/profiles/user_visits_profile_preferences_page_spec.rb
index 4da1a7ba81a..033d69d29b9 100644
--- a/spec/features/profiles/user_visits_profile_preferences_page_spec.rb
+++ b/spec/features/profiles/user_visits_profile_preferences_page_spec.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe 'User visits the profile preferences page', :js, feature_category: :user_profile do
include ListboxHelpers
- let(:user) { create(:user, :no_super_sidebar) }
+ let(:user) { create(:user) }
before do
sign_in(user)
@@ -13,10 +13,6 @@ RSpec.describe 'User visits the profile preferences page', :js, feature_category
visit(profile_preferences_path)
end
- it 'shows correct menu item' do
- expect(page).to have_active_navigation('Preferences')
- end
-
describe 'User changes their syntax highlighting theme', :js do
it 'updates their preference' do
choose 'user_color_scheme_id_5'
@@ -44,7 +40,7 @@ RSpec.describe 'User visits the profile preferences page', :js, feature_category
wait_for_requests
- find('#logo').click
+ find('[data-track-label="gitlab_logo_link"]').click
expect(page).to have_content("You don't have starred projects yet")
expect(page).to have_current_path starred_dashboard_projects_path, ignore_query: true
diff --git a/spec/features/profiles/user_visits_profile_spec.rb b/spec/features/profiles/user_visits_profile_spec.rb
index 821c3d5ef2b..37a19ecadb8 100644
--- a/spec/features/profiles/user_visits_profile_spec.rb
+++ b/spec/features/profiles/user_visits_profile_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe 'User visits their profile', feature_category: :user_profile do
- let_it_be_with_refind(:user) { create(:user, :no_super_sidebar) }
+ let_it_be_with_refind(:user) { create(:user) }
before do
stub_feature_flags(profile_tabs_vue: false)
@@ -11,12 +11,6 @@ RSpec.describe 'User visits their profile', feature_category: :user_profile do
sign_in(user)
end
- it 'shows correct menu item' do
- visit(profile_path)
-
- expect(page).to have_active_navigation('Profile')
- end
-
it 'shows profile info' do
visit(profile_path)
@@ -59,7 +53,7 @@ RSpec.describe 'User visits their profile', feature_category: :user_profile do
expect(page).to have_content user.username
end
- page.within ".content" do
+ within_testid('super-sidebar') do
click_link link
end
diff --git a/spec/features/profiles/user_visits_profile_ssh_keys_page_spec.rb b/spec/features/profiles/user_visits_profile_ssh_keys_page_spec.rb
deleted file mode 100644
index 728fe1a3172..00000000000
--- a/spec/features/profiles/user_visits_profile_ssh_keys_page_spec.rb
+++ /dev/null
@@ -1,17 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe 'User visits the profile SSH keys page', feature_category: :user_profile do
- let(:user) { create(:user, :no_super_sidebar) }
-
- before do
- sign_in(user)
-
- visit(profile_keys_path)
- end
-
- it 'shows correct menu item' do
- expect(page).to have_active_navigation('SSH Keys')
- end
-end
diff --git a/spec/features/project_variables_spec.rb b/spec/features/project_variables_spec.rb
index e2fa924af67..9a1aa41b982 100644
--- a/spec/features/project_variables_spec.rb
+++ b/spec/features/project_variables_spec.rb
@@ -13,13 +13,13 @@ RSpec.describe 'Project variables', :js, feature_category: :secrets_management d
project.add_maintainer(user)
project.variables << variable
- stub_feature_flags(ci_variable_drawer: false)
visit page_path
wait_for_requests
end
context 'when ci_variables_pages FF is enabled' do
- it_behaves_like 'variable list'
+ it_behaves_like 'variable list drawer'
+ it_behaves_like 'variable list env scope'
it_behaves_like 'variable list pagination', :ci_variable
end
@@ -28,37 +28,7 @@ RSpec.describe 'Project variables', :js, feature_category: :secrets_management d
stub_feature_flags(ci_variables_pages: false)
end
- it_behaves_like 'variable list'
- end
-
- it 'adds a new variable with an environment scope' do
- click_button('Add variable')
-
- page.within('#add-ci-variable') do
- fill_in 'Key', with: 'akey'
- find('#ci-variable-value').set('akey_value')
-
- click_button('All (default)')
- fill_in 'Search', with: 'review/*'
- find('[data-testid="create-wildcard-button"]').click
-
- click_button('Add variable')
- end
-
- wait_for_requests
-
- page.within('[data-testid="ci-variable-table"]') do
- expect(find('.js-ci-variable-row:first-child [data-label="Environments"]').text).to eq('review/*')
- end
- end
-
- context 'when ci_variable_drawer FF is enabled' do
- before do
- stub_feature_flags(ci_variable_drawer: true)
- visit page_path
- wait_for_requests
- end
-
it_behaves_like 'variable list drawer'
+ it_behaves_like 'variable list env scope'
end
end
diff --git a/spec/features/projects/active_tabs_spec.rb b/spec/features/projects/active_tabs_spec.rb
index 8879636e4dc..199ea638f61 100644
--- a/spec/features/projects/active_tabs_spec.rb
+++ b/spec/features/projects/active_tabs_spec.rb
@@ -2,8 +2,8 @@
require 'spec_helper'
-RSpec.describe 'Project active tab', feature_category: :groups_and_projects do
- let_it_be(:user) { create(:user, :no_super_sidebar) }
+RSpec.describe 'Project active tab', :js, feature_category: :groups_and_projects do
+ let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project, :repository, :with_namespace_settings, namespace: user.namespace) }
before do
@@ -11,7 +11,7 @@ RSpec.describe 'Project active tab', feature_category: :groups_and_projects do
end
def click_tab(title)
- page.within '.sidebar-top-level-items > .active' do
+ within_testid('super-sidebar') do
click_link(title)
end
end
@@ -20,66 +20,68 @@ RSpec.describe 'Project active tab', feature_category: :groups_and_projects do
it 'activates Project scope menu' do
visit project_path(project)
- expect(page).to have_selector('.sidebar-top-level-items > li.active', count: 1)
- expect(find('.sidebar-top-level-items > li.active')).to have_content(project.name)
+ expect(page).to have_selector('a[aria-current="page"]', count: 1)
+ expect(find('a[aria-current="page"]')).to have_content(project.name)
end
end
- context 'on Project information' do
- context 'default link' do
- before do
- visit project_path(project)
-
- click_link('Project information', match: :first)
- end
-
- it_behaves_like 'page has active tab', 'Project'
- it_behaves_like 'page has active sub tab', 'Activity'
- end
+ context 'on Project Manage' do
+ %w[Activity Members Labels].each do |sub_menu|
+ context "on project Manage/#{sub_menu}" do
+ before do
+ visit project_path(project)
+ within_testid('super-sidebar') do
+ click_button("Manage")
+ end
+ click_tab(sub_menu)
+ end
- context 'on Project information/Activity' do
- before do
- visit activity_project_path(project)
+ it_behaves_like 'page has active tab', 'Manage'
+ it_behaves_like 'page has active sub tab', sub_menu
end
-
- it_behaves_like 'page has active tab', 'Project'
- it_behaves_like 'page has active sub tab', 'Activity'
end
end
- context 'on project Repository' do
+ context 'on project Code' do
before do
root_ref = project.repository.root_ref
visit project_tree_path(project, root_ref)
+
+ # Enabling Js in here causes more SQL queries to be caught by the query limiter.
+ # We are increasing the limit here so that the tests pass.
+ allow(Gitlab::QueryLimiting::Transaction).to receive(:threshold).and_return(110)
end
- it_behaves_like 'page has active tab', 'Repository'
+ it_behaves_like 'page has active tab', 'Code'
- %w[Files Commits Graph Compare Branches Tags].each do |sub_menu|
- context "on project Repository/#{sub_menu}" do
+ ["Repository", "Branches", "Commits", "Tags", "Repository graph", "Compare revisions"].each do |sub_menu|
+ context "on project Code/#{sub_menu}" do
before do
click_tab(sub_menu)
end
- it_behaves_like 'page has active tab', 'Repository'
+ it_behaves_like 'page has active tab', 'Code'
it_behaves_like 'page has active sub tab', sub_menu
end
end
end
- context 'on project Issues' do
+ context 'on project Plan' do
before do
visit project_issues_path(project)
end
- it_behaves_like 'page has active tab', 'Issues'
+ it_behaves_like 'page has active tab', 'Pinned'
- context "on project Issues/Milestones" do
+ context "on project Code/Milestones" do
before do
+ within_testid('super-sidebar') do
+ click_button("Plan")
+ end
click_tab('Milestones')
end
- it_behaves_like 'page has active tab', 'Issues'
+ it_behaves_like 'page has active tab', 'Plan'
it_behaves_like 'page has active sub tab', 'Milestones'
end
end
@@ -89,7 +91,7 @@ RSpec.describe 'Project active tab', feature_category: :groups_and_projects do
visit project_merge_requests_path(project)
end
- it_behaves_like 'page has active tab', 'Merge requests'
+ it_behaves_like 'page has active tab', 'Pinned'
end
context 'on project Wiki' do
@@ -97,7 +99,8 @@ RSpec.describe 'Project active tab', feature_category: :groups_and_projects do
visit wiki_path(project.wiki)
end
- it_behaves_like 'page has active tab', 'Wiki'
+ it_behaves_like 'page has active tab', 'Plan'
+ it_behaves_like 'page has active sub tab', 'Wiki'
end
context 'on project Members' do
@@ -105,7 +108,8 @@ RSpec.describe 'Project active tab', feature_category: :groups_and_projects do
visit project_project_members_path(project)
end
- it_behaves_like 'page has active tab', 'Members'
+ it_behaves_like 'page has active tab', 'Manage'
+ it_behaves_like 'page has active sub tab', 'Members'
end
context 'on project Settings' do
@@ -132,23 +136,23 @@ RSpec.describe 'Project active tab', feature_category: :groups_and_projects do
end
end
- context 'on project Analytics' do
+ context 'on project Analyze' do
before do
visit project_cycle_analytics_path(project)
end
- context 'on project Analytics/Value stream Analytics' do
- it_behaves_like 'page has active tab', _('Analytics')
+ context 'on project Analyze/Value stream Analyze' do
+ it_behaves_like 'page has active tab', _('Analyze')
it_behaves_like 'page has active sub tab', _('Value stream')
end
- context 'on project Analytics/"CI/CD"' do
+ context 'on project Analyze/"CI/CD"' do
before do
click_tab(_('CI/CD'))
end
- it_behaves_like 'page has active tab', _('Analytics')
- it_behaves_like 'page has active sub tab', _('CI/CD')
+ it_behaves_like 'page has active tab', _('Analyze')
+ it_behaves_like 'page has active sub tab', _('CI/CD analytics')
end
end
@@ -161,7 +165,7 @@ RSpec.describe 'Project active tab', feature_category: :groups_and_projects do
visit project_pipeline_path(project, pipeline)
end
- it_behaves_like 'page has active tab', _('CI/CD')
+ it_behaves_like 'page has active tab', _('Build')
it_behaves_like 'page has active sub tab', _('Pipelines')
end
@@ -170,7 +174,7 @@ RSpec.describe 'Project active tab', feature_category: :groups_and_projects do
visit dag_project_pipeline_path(project, pipeline)
end
- it_behaves_like 'page has active tab', _('CI/CD')
+ it_behaves_like 'page has active tab', _('Build')
it_behaves_like 'page has active sub tab', _('Pipelines')
end
@@ -179,7 +183,7 @@ RSpec.describe 'Project active tab', feature_category: :groups_and_projects do
visit builds_project_pipeline_path(project, pipeline)
end
- it_behaves_like 'page has active tab', _('CI/CD')
+ it_behaves_like 'page has active tab', _('Build')
it_behaves_like 'page has active sub tab', _('Pipelines')
end
@@ -188,7 +192,7 @@ RSpec.describe 'Project active tab', feature_category: :groups_and_projects do
visit failures_project_pipeline_path(project, pipeline)
end
- it_behaves_like 'page has active tab', _('CI/CD')
+ it_behaves_like 'page has active tab', _('Build')
it_behaves_like 'page has active sub tab', _('Pipelines')
end
@@ -197,7 +201,7 @@ RSpec.describe 'Project active tab', feature_category: :groups_and_projects do
visit test_report_project_pipeline_path(project, pipeline)
end
- it_behaves_like 'page has active tab', _('CI/CD')
+ it_behaves_like 'page has active tab', _('Build')
it_behaves_like 'page has active sub tab', _('Pipelines')
end
end
diff --git a/spec/features/projects/blobs/blob_show_spec.rb b/spec/features/projects/blobs/blob_show_spec.rb
index 30a81ccc071..36665f2b77d 100644
--- a/spec/features/projects/blobs/blob_show_spec.rb
+++ b/spec/features/projects/blobs/blob_show_spec.rb
@@ -941,9 +941,7 @@ RSpec.describe 'File blob', :js, feature_category: :groups_and_projects do
it 'shows the realtime pipeline status' do
page.within('.commit-actions') do
- expect(page).to have_css('.ci-status-icon')
- expect(page).to have_css('.ci-status-icon-running')
- expect(page).to have_selector('[data-testid="status_running-icon"]')
+ expect(page).to have_selector('[data-testid="status_running_borderless-icon"]')
end
end
end
diff --git a/spec/features/projects/blobs/edit_spec.rb b/spec/features/projects/blobs/edit_spec.rb
index e8a9edcc0cc..9c4f70a68b8 100644
--- a/spec/features/projects/blobs/edit_spec.rb
+++ b/spec/features/projects/blobs/edit_spec.rb
@@ -120,7 +120,7 @@ RSpec.describe 'Editing file blob', :js, feature_category: :groups_and_projects
it 'updates content' do
edit_and_commit
- expect(page).to have_content 'successfully committed'
+ expect(page).to have_content 'committed successfully.'
expect(page).to have_content 'NextFeature'
end
diff --git a/spec/features/projects/branches/user_creates_branch_spec.rb b/spec/features/projects/branches/user_creates_branch_spec.rb
index eafb75d75ac..8d636dacb75 100644
--- a/spec/features/projects/branches/user_creates_branch_spec.rb
+++ b/spec/features/projects/branches/user_creates_branch_spec.rb
@@ -6,7 +6,7 @@ RSpec.describe 'User creates branch', :js, feature_category: :groups_and_project
include Features::BranchesHelpers
let_it_be(:group) { create(:group, :public) }
- let_it_be(:user) { create(:user, :no_super_sidebar) }
+ let_it_be(:user) { create(:user) }
shared_examples 'creates new branch' do
specify do
diff --git a/spec/features/projects/branches_spec.rb b/spec/features/projects/branches_spec.rb
index 79e9ca7998e..7915f446ee0 100644
--- a/spec/features/projects/branches_spec.rb
+++ b/spec/features/projects/branches_spec.rb
@@ -299,13 +299,13 @@ RSpec.describe 'Branches', feature_category: :groups_and_projects do
it 'shows pipeline status when available' do
page.within first('.all-branches li') do
- expect(page).to have_css 'a.gl-badge .ci-status-icon-success'
+ expect(page).to have_css '[data-testid="status_success_borderless-icon"]'
end
end
it 'displays a placeholder when not available' do
page.all('.all-branches li') do |li|
- expect(li).to have_css '.pipeline-status svg.s16'
+ expect(li).to have_css '.pipeline-status svg.s24'
end
end
end
@@ -317,7 +317,7 @@ RSpec.describe 'Branches', feature_category: :groups_and_projects do
it 'does not show placeholder or pipeline status' do
page.all('.all-branches') do |branches|
- expect(branches).not_to have_css '.pipeline-status svg.s16'
+ expect(branches).not_to have_css '.pipeline-status svg.s24'
end
end
end
diff --git a/spec/features/projects/ci/editor_spec.rb b/spec/features/projects/ci/editor_spec.rb
index adaa5e48967..22cc5c67987 100644
--- a/spec/features/projects/ci/editor_spec.rb
+++ b/spec/features/projects/ci/editor_spec.rb
@@ -4,6 +4,7 @@ require 'spec_helper'
RSpec.describe 'Pipeline Editor', :js, feature_category: :pipeline_composition do
include Features::SourceEditorSpecHelpers
+ include ListboxHelpers
let(:project) { create(:project_empty_repo, :public) }
let(:user) { create(:user) }
@@ -216,12 +217,13 @@ RSpec.describe 'Pipeline Editor', :js, feature_category: :pipeline_composition d
def switch_to_branch(branch)
# close button for the popover
find('[data-testid="close-button"]').click
- find('[data-testid="branch-selector"]').click
page.within '[data-testid="branch-selector"]' do
- click_button branch
- wait_for_requests
+ toggle_listbox
+ select_listbox_item(branch, exact_text: true)
end
+
+ wait_for_requests
end
before do
diff --git a/spec/features/projects/clusters/gcp_spec.rb b/spec/features/projects/clusters/gcp_spec.rb
index b16f43a16b6..c223053606b 100644
--- a/spec/features/projects/clusters/gcp_spec.rb
+++ b/spec/features/projects/clusters/gcp_spec.rb
@@ -6,7 +6,7 @@ RSpec.describe 'Gcp Cluster', :js, feature_category: :deployment_management do
include GoogleApi::CloudPlatformHelpers
let(:project) { create(:project) }
- let(:user) { create(:user, :no_super_sidebar) }
+ let(:user) { create(:user) }
before do
project.add_maintainer(user)
@@ -54,7 +54,7 @@ RSpec.describe 'Gcp Cluster', :js, feature_category: :deployment_management do
before do
visit project_clusters_path(project)
- click_button(class: 'gl-new-dropdown-toggle')
+ click_button(class: 'gl-new-dropdown-toggle', text: 'Connect a cluster (agent)')
click_link 'Connect a cluster (certificate - deprecated)'
end
diff --git a/spec/features/projects/clusters/user_spec.rb b/spec/features/projects/clusters/user_spec.rb
index 1393cc6db15..e256b44c4dc 100644
--- a/spec/features/projects/clusters/user_spec.rb
+++ b/spec/features/projects/clusters/user_spec.rb
@@ -6,7 +6,7 @@ RSpec.describe 'User Cluster', :js, feature_category: :deployment_management do
include GoogleApi::CloudPlatformHelpers
let(:project) { create(:project) }
- let(:user) { create(:user, :no_super_sidebar) }
+ let(:user) { create(:user) }
before do
project.add_maintainer(user)
@@ -25,7 +25,7 @@ RSpec.describe 'User Cluster', :js, feature_category: :deployment_management do
before do
visit project_clusters_path(project)
- click_button(class: 'gl-new-dropdown-toggle')
+ click_button(class: 'gl-new-dropdown-toggle', text: 'Connect a cluster (agent)')
click_link 'Connect a cluster (certificate - deprecated)'
end
diff --git a/spec/features/projects/clusters_spec.rb b/spec/features/projects/clusters_spec.rb
index c5d960f2308..d799fbc49ef 100644
--- a/spec/features/projects/clusters_spec.rb
+++ b/spec/features/projects/clusters_spec.rb
@@ -6,7 +6,7 @@ RSpec.describe 'Clusters', :js, feature_category: :groups_and_projects do
include GoogleApi::CloudPlatformHelpers
let(:project) { create(:project) }
- let(:user) { create(:user, :no_super_sidebar) }
+ let(:user) { create(:user) }
before do
project.add_maintainer(user)
@@ -125,12 +125,12 @@ RSpec.describe 'Clusters', :js, feature_category: :groups_and_projects do
def visit_create_cluster_page
visit project_clusters_path(project)
- click_button(class: 'gl-new-dropdown-toggle')
+ click_button(class: 'gl-new-dropdown-toggle', text: 'Connect a cluster (agent)')
click_link 'Create a cluster'
end
def visit_connect_cluster_page
- click_button(class: 'gl-new-dropdown-toggle')
+ click_button(class: 'gl-new-dropdown-toggle', text: 'Connect a cluster (agent)')
click_link 'Connect a cluster (certificate - deprecated)'
end
end
diff --git a/spec/features/projects/commit/comments/user_adds_comment_spec.rb b/spec/features/projects/commit/comments/user_adds_comment_spec.rb
index b0cb57f158d..d9225192f6b 100644
--- a/spec/features/projects/commit/comments/user_adds_comment_spec.rb
+++ b/spec/features/projects/commit/comments/user_adds_comment_spec.rb
@@ -112,7 +112,7 @@ RSpec.describe "User adds a comment on a commit", :js, feature_category: :source
click_button("Comment")
end
- expect(page).to have_button("Reply...").and have_no_css("form.new_note")
+ expect(page).to have_css(".reply-placeholder-text-field").and have_no_css("form.new_note")
end
# A comment should be added and visible.
diff --git a/spec/features/projects/commit/mini_pipeline_graph_spec.rb b/spec/features/projects/commit/mini_pipeline_graph_spec.rb
index 5bb3d1af924..c0a8dabf648 100644
--- a/spec/features/projects/commit/mini_pipeline_graph_spec.rb
+++ b/spec/features/projects/commit/mini_pipeline_graph_spec.rb
@@ -52,7 +52,7 @@ RSpec.describe 'Mini Pipeline Graph in Commit View', :js, feature_category: :sou
end
it 'display icon with status' do
- expect(page).to have_selector('.ci-status-icon-running')
+ expect(page).to have_selector('[data-testid="status_running_borderless-icon"]')
end
it 'displays a mini pipeline graph' do
@@ -63,7 +63,7 @@ RSpec.describe 'Mini Pipeline Graph in Commit View', :js, feature_category: :sou
wait_for_requests
page.within '.js-builds-dropdown-list' do
- expect(page).to have_selector('.ci-status-icon-running')
+ expect(page).to have_selector('[data-testid="status_running_borderless-icon"]')
expect(page).to have_content(build.stage_name)
end
diff --git a/spec/features/projects/commit/user_sees_pipelines_tab_spec.rb b/spec/features/projects/commit/user_sees_pipelines_tab_spec.rb
index 00cb5474ea0..5d722ddbedb 100644
--- a/spec/features/projects/commit/user_sees_pipelines_tab_spec.rb
+++ b/spec/features/projects/commit/user_sees_pipelines_tab_spec.rb
@@ -36,9 +36,8 @@ RSpec.describe 'Commit > Pipelines tab', :js, feature_category: :source_code_man
wait_for_requests
page.within('[data-testid="pipeline-table-row"]') do
- expect(page).to have_selector('[data-testid="ci-badge-link"]', text: 'Passed')
+ expect(page).to have_selector('[data-testid="ci-icon"]', text: 'Passed')
expect(page).to have_content(pipeline.id)
- expect(page).to have_content('API')
expect(page).to have_css('[data-testid="pipeline-mini-graph"]')
expect(page).to have_css('[data-testid="pipelines-manual-actions-dropdown"]')
expect(page).to have_css('[data-testid="pipeline-multi-actions-dropdown"]')
diff --git a/spec/features/projects/compare_spec.rb b/spec/features/projects/compare_spec.rb
index eff538513c1..ccf5c6996f1 100644
--- a/spec/features/projects/compare_spec.rb
+++ b/spec/features/projects/compare_spec.rb
@@ -185,13 +185,4 @@ RSpec.describe "Compare", :js, feature_category: :groups_and_projects do
it_behaves_like "compare view of branches"
it_behaves_like "compare view of tags"
-
- context "when super sidebar is enabled" do
- before do
- user.update!(use_new_navigation: true)
- end
-
- it_behaves_like "compare view of branches"
- it_behaves_like "compare view of tags"
- end
end
diff --git a/spec/features/projects/confluence/user_views_confluence_page_spec.rb b/spec/features/projects/confluence/user_views_confluence_page_spec.rb
index 216bea74c09..9c036f35887 100644
--- a/spec/features/projects/confluence/user_views_confluence_page_spec.rb
+++ b/spec/features/projects/confluence/user_views_confluence_page_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe 'User views the Confluence page', feature_category: :integrations do
- let_it_be(:user) { create(:user, :no_super_sidebar) }
+ let_it_be(:user) { create(:user) }
let(:project) { create(:project, :public) }
@@ -11,12 +11,14 @@ RSpec.describe 'User views the Confluence page', feature_category: :integrations
sign_in(user)
end
- it 'shows the page when the Confluence integration is enabled' do
+ it 'shows the page when the Confluence integration is enabled', :js do
service = create(:confluence_integration, project: project)
visit project_wikis_confluence_path(project)
- expect(page).to have_css('.nav-sidebar li.active', text: 'Confluence', match: :first)
+ within_testid('super-sidebar') do
+ expect(page).to have_css('a[aria-current="page"]', text: 'Confluence')
+ end
element = page.find('.row.empty-state')
diff --git a/spec/features/projects/environments/environments_spec.rb b/spec/features/projects/environments/environments_spec.rb
index 3a2c7f0ac7b..0a54f5923f2 100644
--- a/spec/features/projects/environments/environments_spec.rb
+++ b/spec/features/projects/environments/environments_spec.rb
@@ -34,16 +34,16 @@ RSpec.describe 'Environments page', :js, feature_category: :continuous_delivery
describe 'with one available environment' do
let!(:environment) { create(:environment, project: project, state: :available) }
- it 'shows "Available" and "Stopped" tab with links' do
+ it 'shows "Active" and "Stopped" tab with links' do
visit_environments(project)
- expect(page).to have_link(_('Available'))
+ expect(page).to have_link(_('Active'))
expect(page).to have_link(_('Stopped'))
end
- describe 'in available tab page' do
+ describe 'in active tab page' do
it 'shows one environment' do
- visit_environments(project, scope: 'available')
+ visit_environments(project, scope: 'active')
expect(page).to have_link(environment.name, href: project_environment_path(project, environment))
end
@@ -56,7 +56,7 @@ RSpec.describe 'Environments page', :js, feature_category: :continuous_delivery
end
it 'renders second page of pipelines' do
- visit_environments(project, scope: 'available')
+ visit_environments(project, scope: 'active')
find('.page-link.next-page-item').click
wait_for_requests
@@ -85,7 +85,7 @@ RSpec.describe 'Environments page', :js, feature_category: :continuous_delivery
end
it 'shows one environment without error' do
- visit_environments(project, scope: 'available')
+ visit_environments(project, scope: 'active')
expect(page).to have_link(environment.name, href: project_environment_path(project, environment))
end
@@ -95,9 +95,9 @@ RSpec.describe 'Environments page', :js, feature_category: :continuous_delivery
describe 'with one stopped environment' do
let!(:environment) { create(:environment, project: project, state: :stopped) }
- describe 'in available tab page' do
+ describe 'in active tab page' do
it 'shows no environments' do
- visit_environments(project, scope: 'available')
+ visit_environments(project, scope: 'active')
expect(page).to have_content(s_('Environments|Get started with environments'))
end
@@ -122,7 +122,7 @@ RSpec.describe 'Environments page', :js, feature_category: :continuous_delivery
it 'does not show environments and tabs' do
expect(page).to have_content(s_('Environments|Get started with environments'))
- expect(page).not_to have_link(_('Available'))
+ expect(page).not_to have_link(_('Active'))
expect(page).not_to have_link(_('Stopped'))
end
end
@@ -142,7 +142,7 @@ RSpec.describe 'Environments page', :js, feature_category: :continuous_delivery
it 'shows environments names and counters' do
expect(page).to have_link(environment.name, href: project_environment_path(project, environment))
- expect(page).to have_link("#{_('Available')} 1")
+ expect(page).to have_link("#{_('Active')} 1")
expect(page).to have_link("#{_('Stopped')} 0")
end
diff --git a/spec/features/projects/features_visibility_spec.rb b/spec/features/projects/features_visibility_spec.rb
index 8f66b722ead..c6a770cee9e 100644
--- a/spec/features/projects/features_visibility_spec.rb
+++ b/spec/features/projects/features_visibility_spec.rb
@@ -3,10 +3,15 @@
require 'spec_helper'
RSpec.describe 'Edit Project Settings', feature_category: :groups_and_projects do
- let(:member) { create(:user, :no_super_sidebar) }
+ let(:member) { create(:user) }
let!(:project) { create(:project, :public, :repository) }
let!(:issue) { create(:issue, project: project) }
- let(:non_member) { create(:user, :no_super_sidebar) }
+ let(:non_member) { create(:user) }
+
+ # Sidebar nav links are only visible after hovering over or expanding the
+ # section that contains them (if it exists). Finding visible and hidden
+ # nav links allows us to avoid doing that.
+ let(:visibility_all) { { visible: :all } }
describe 'project features visibility selectors', :js do
before do
@@ -14,7 +19,7 @@ RSpec.describe 'Edit Project Settings', feature_category: :groups_and_projects d
sign_in(member)
end
- tools = { builds: "pipelines", issues: "issues", wiki: "wiki", snippets: "snippets", merge_requests: "merge_requests", analytics: "analytics" }
+ tools = { builds: "pipelines", issues: "issues", wiki: "wiki", snippets: "snippets", merge_requests: "merge_requests", analytics: "project-cycle-analytics" }
tools.each do |tool_name, shortcut_name|
describe "feature #{tool_name}" do
@@ -22,20 +27,16 @@ RSpec.describe 'Edit Project Settings', feature_category: :groups_and_projects d
visit edit_project_path(project)
# disable by clicking toggle
- toggle_feature_off("project[project_feature_attributes][#{tool_name}_access_level]")
- page.within('.sharing-permissions') do
- find('[data-testid="project-features-save-button"]').click
- end
+ toggle_feature_off(tool_name)
+ click_save_changes
wait_for_requests
- expect(page).not_to have_selector(".shortcuts-#{shortcut_name}")
+ expect(page).not_to have_selector(".shortcuts-#{shortcut_name}", **visibility_all)
# re-enable by clicking toggle again
- toggle_feature_on("project[project_feature_attributes][#{tool_name}_access_level]")
- page.within('.sharing-permissions') do
- find('[data-testid="project-features-save-button"]').click
- end
+ toggle_feature_on(tool_name)
+ click_save_changes
wait_for_requests
- expect(page).to have_selector(".shortcuts-#{shortcut_name}")
+ expect(page).to have_selector(".shortcuts-#{shortcut_name}", **visibility_all)
end
end
end
@@ -48,8 +49,8 @@ RSpec.describe 'Edit Project Settings', feature_category: :groups_and_projects d
visit project_path(project)
- expect(page).to have_selector('.shortcuts-issues')
- expect(page).not_to have_selector('.shortcuts-labels')
+ expect(page).to have_selector('.shortcuts-issues', **visibility_all)
+ expect(page).not_to have_selector('.shortcuts-labels', **visibility_all)
end
end
@@ -65,8 +66,8 @@ RSpec.describe 'Edit Project Settings', feature_category: :groups_and_projects d
it 'hides issues tab' do
visit project_path(project)
- expect(page).not_to have_selector('.shortcuts-issues')
- expect(page).not_to have_selector('.shortcuts-labels')
+ expect(page).not_to have_selector('.shortcuts-issues', **visibility_all)
+ expect(page).not_to have_selector('.shortcuts-labels', **visibility_all)
end
end
@@ -74,7 +75,7 @@ RSpec.describe 'Edit Project Settings', feature_category: :groups_and_projects d
it "shows builds when enabled" do
visit project_pipelines_path(project)
- expect(page).to have_selector(".shortcuts-builds")
+ expect(page).to have_selector(".shortcuts-builds", **visibility_all)
end
it "hides builds when disabled" do
@@ -83,7 +84,7 @@ RSpec.describe 'Edit Project Settings', feature_category: :groups_and_projects d
visit project_pipelines_path(project)
- expect(page).not_to have_selector(".shortcuts-builds")
+ expect(page).not_to have_selector(".shortcuts-builds", **visibility_all)
end
end
end
@@ -183,23 +184,19 @@ RSpec.describe 'Edit Project Settings', feature_category: :groups_and_projects d
end
it "disables repository related features" do
- toggle_feature_off('project[project_feature_attributes][repository_access_level]')
+ toggle_feature_off('repository')
- page.within('.sharing-permissions') do
- click_button "Save changes"
- end
+ click_save_changes
expect(find(".sharing-permissions")).to have_selector(".gl-toggle.is-disabled", minimum: 3)
end
it "shows empty features project homepage" do
- toggle_feature_off('project[project_feature_attributes][repository_access_level]')
- toggle_feature_off('project[project_feature_attributes][issues_access_level]')
- toggle_feature_off('project[project_feature_attributes][wiki_access_level]')
+ toggle_feature_off('repository')
+ toggle_feature_off('issues')
+ toggle_feature_off('wiki')
- page.within('.sharing-permissions') do
- click_button "Save changes"
- end
+ click_save_changes
wait_for_requests
visit project_path(project)
@@ -208,13 +205,11 @@ RSpec.describe 'Edit Project Settings', feature_category: :groups_and_projects d
end
it "hides project activity tabs" do
- toggle_feature_off('project[project_feature_attributes][repository_access_level]')
- toggle_feature_off('project[project_feature_attributes][issues_access_level]')
- toggle_feature_off('project[project_feature_attributes][wiki_access_level]')
+ toggle_feature_off('repository')
+ toggle_feature_off('issues')
+ toggle_feature_off('wiki')
- page.within('.sharing-permissions') do
- click_button "Save changes"
- end
+ click_save_changes
wait_for_requests
visit activity_project_path(project)
@@ -229,7 +224,7 @@ RSpec.describe 'Edit Project Settings', feature_category: :groups_and_projects d
# Regression spec for https://gitlab.com/gitlab-org/gitlab-foss/issues/25272
it "hides comments activity tab only on disabled issues, merge requests and repository" do
- toggle_feature_off('project[project_feature_attributes][issues_access_level]')
+ toggle_feature_off('issues')
save_changes_and_check_activity_tab do
expect(page).to have_content("Comments")
@@ -237,7 +232,7 @@ RSpec.describe 'Edit Project Settings', feature_category: :groups_and_projects d
visit edit_project_path(project)
- toggle_feature_off('project[project_feature_attributes][merge_requests_access_level]')
+ toggle_feature_off('merge_requests')
save_changes_and_check_activity_tab do
expect(page).to have_content("Comments")
@@ -245,7 +240,7 @@ RSpec.describe 'Edit Project Settings', feature_category: :groups_and_projects d
visit edit_project_path(project)
- toggle_feature_off('project[project_feature_attributes][repository_access_level]')
+ toggle_feature_off('repository')
save_changes_and_check_activity_tab do
expect(page).not_to have_content("Comments")
@@ -255,9 +250,7 @@ RSpec.describe 'Edit Project Settings', feature_category: :groups_and_projects d
end
def save_changes_and_check_activity_tab
- page.within('.sharing-permissions') do
- click_button "Save changes"
- end
+ click_save_changes
wait_for_requests
visit activity_project_path(project)
@@ -284,10 +277,16 @@ RSpec.describe 'Edit Project Settings', feature_category: :groups_and_projects d
end
def toggle_feature_off(feature_name)
- find(".project-feature-controls[data-for=\"#{feature_name}\"] .gl-toggle.is-checked").click
+ find(".project-feature-controls[data-for=\"project[project_feature_attributes][#{feature_name}_access_level]\"] .gl-toggle.is-checked").click
end
def toggle_feature_on(feature_name)
- find(".project-feature-controls[data-for=\"#{feature_name}\"] .gl-toggle:not(.is-checked)").click
+ find(".project-feature-controls[data-for=\"project[project_feature_attributes][#{feature_name}_access_level]\"] .gl-toggle:not(.is-checked)").click
+ end
+
+ def click_save_changes
+ page.within('.sharing-permissions') do
+ click_button 'Save changes'
+ end
end
end
diff --git a/spec/features/projects/files/project_owner_creates_license_file_spec.rb b/spec/features/projects/files/project_owner_creates_license_file_spec.rb
index 595aad0144b..18041bbb00a 100644
--- a/spec/features/projects/files/project_owner_creates_license_file_spec.rb
+++ b/spec/features/projects/files/project_owner_creates_license_file_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe 'Projects > Files > Project owner creates a license file', :js, feature_category: :groups_and_projects do
- let_it_be(:project_maintainer) { create(:user, :no_super_sidebar) }
+ let_it_be(:project_maintainer) { create(:user) }
let_it_be(:project) { create(:project, :repository, namespace: project_maintainer.namespace) }
before do
@@ -59,7 +59,7 @@ RSpec.describe 'Projects > Files > Project owner creates a license file', :js, f
end
def select_template(template)
- page.within('.gl-new-dropdown') do
+ within_testid('template-selector') do
click_button 'Apply a template'
find('.gl-new-dropdown-contents li', text: template).click
wait_for_requests
diff --git a/spec/features/projects/files/user_edits_files_spec.rb b/spec/features/projects/files/user_edits_files_spec.rb
index 10fa4a21359..5612f6a53b2 100644
--- a/spec/features/projects/files/user_edits_files_spec.rb
+++ b/spec/features/projects/files/user_edits_files_spec.rb
@@ -79,6 +79,25 @@ RSpec.describe 'Projects > Files > User edits files', :js, feature_category: :gr
expect(page).to have_content('*.rbca')
end
+ it 'displays a flash message with a link when an edited file was committed' do
+ click_link('.gitignore')
+ edit_in_single_file_editor
+ find('.file-editor', match: :first)
+
+ editor_set_value('*.rbca')
+ fill_in(:commit_message, with: 'New commit message', visible: true)
+ click_button('Commit changes')
+
+ expect(page).to have_current_path(project_blob_path(project, 'master/.gitignore'), ignore_query: true)
+
+ wait_for_requests
+
+ expect(page).to have_content('Your changes have been committed successfully')
+ page.within '.flash-container' do
+ expect(page).to have_link 'changes'
+ end
+ end
+
it 'commits an edited file to a new branch' do
click_link('.gitignore')
edit_in_single_file_editor
diff --git a/spec/features/projects/files/user_find_file_spec.rb b/spec/features/projects/files/user_find_file_spec.rb
index 005a870bea0..b6e739e8082 100644
--- a/spec/features/projects/files/user_find_file_spec.rb
+++ b/spec/features/projects/files/user_find_file_spec.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe 'User find project file', feature_category: :groups_and_projects do
include ListboxHelpers
- let(:user) { create :user, :no_super_sidebar }
+ let(:user) { create :user }
let(:project) { create :project, :repository }
before do
@@ -15,29 +15,25 @@ RSpec.describe 'User find project file', feature_category: :groups_and_projects
visit project_tree_path(project, project.repository.root_ref)
end
- def active_main_tab
- find('.sidebar-top-level-items > li.active')
- end
-
def find_file(text)
fill_in 'file_find', with: text
end
def ref_selector_dropdown
- find('.gl-button-text')
+ find('.ref-selector .gl-button-text')
end
it 'navigates to find file by shortcut', :js do
find('body').native.send_key('t')
- expect(active_main_tab).to have_content('Repository')
+ expect(page).to have_active_sub_navigation('Repository')
expect(page).to have_selector('.file-finder-holder', count: 1)
end
- it 'navigates to find file' do
+ it 'navigates to find file', :js do
click_link 'Find file'
- expect(active_main_tab).to have_content('Repository')
+ expect(page).to have_active_sub_navigation('Repository')
expect(page).to have_selector('.file-finder-holder', count: 1)
end
diff --git a/spec/features/projects/files/user_reads_pipeline_status_spec.rb b/spec/features/projects/files/user_reads_pipeline_status_spec.rb
index ce3f0541139..24dd673501c 100644
--- a/spec/features/projects/files/user_reads_pipeline_status_spec.rb
+++ b/spec/features/projects/files/user_reads_pipeline_status_spec.rb
@@ -25,7 +25,7 @@ RSpec.describe 'user reads pipeline status', :js, feature_category: :groups_and_
page.within('.commit-detail') do
expect(page).to have_link('', href: project_pipeline_path(project, expected_pipeline))
- expect(page).to have_selector(".ci-status-icon-#{expected_pipeline.status}")
+ expect(page).to have_selector("[data-testid='status_#{expected_pipeline.status}_borderless-icon']")
end
end
end
diff --git a/spec/features/projects/files/user_searches_for_files_spec.rb b/spec/features/projects/files/user_searches_for_files_spec.rb
index 627912df408..030d5a8ec40 100644
--- a/spec/features/projects/files/user_searches_for_files_spec.rb
+++ b/spec/features/projects/files/user_searches_for_files_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe 'Projects > Files > User searches for files', feature_category: :groups_and_projects do
- let_it_be(:user) { create(:user, :no_super_sidebar) }
+ let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project, :repository, namespace: user.namespace) }
before do
@@ -18,7 +18,7 @@ RSpec.describe 'Projects > Files > User searches for files', feature_category: :
visit project_path(project)
end
- it 'does not show any result' do
+ it 'does not show any result', :js do
submit_search('coffee')
expect(page).to have_content("We couldn't find any")
@@ -41,7 +41,7 @@ RSpec.describe 'Projects > Files > User searches for files', feature_category: :
visit project_tree_path(project, project.default_branch)
end
- it 'shows found files' do
+ it 'shows found files', :js do
expect(page).to have_selector('.tree-controls .shortcuts-find-file')
submit_search('coffee')
diff --git a/spec/features/projects/forks/fork_list_spec.rb b/spec/features/projects/forks/fork_list_spec.rb
index 86e4e03259e..966147637f5 100644
--- a/spec/features/projects/forks/fork_list_spec.rb
+++ b/spec/features/projects/forks/fork_list_spec.rb
@@ -8,7 +8,7 @@ RSpec.describe 'listing forks of a project', feature_category: :groups_and_proje
let(:source) { create(:project, :public, :repository) }
let!(:fork) { fork_project(source, nil, repository: true) }
- let(:user) { create(:user, :no_super_sidebar) }
+ let(:user) { create(:user) }
before do
source.add_maintainer(user)
diff --git a/spec/features/projects/graph_spec.rb b/spec/features/projects/graph_spec.rb
index 9b0803e4b0c..e9c05fd7f7f 100644
--- a/spec/features/projects/graph_spec.rb
+++ b/spec/features/projects/graph_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe 'Project Graph', :js, feature_category: :groups_and_projects do
- let(:user) { create(:user, :no_super_sidebar) }
+ let(:user) { create(:user) }
let(:project) { create(:project, :repository, namespace: user.namespace) }
let(:branch_name) { 'master' }
@@ -59,7 +59,7 @@ RSpec.describe 'Project Graph', :js, feature_category: :groups_and_projects do
it 'HTML escapes branch name' do
expect(page.body).to include("Commit statistics for <strong>#{ERB::Util.html_escape(branch_name)}</strong>")
- expect(page.find('.gl-new-dropdown-button-text')['innerHTML']).to include(ERB::Util.html_escape(branch_name))
+ expect(page).to have_button(branch_name)
end
end
diff --git a/spec/features/projects/integrations/user_activates_issue_tracker_spec.rb b/spec/features/projects/integrations/user_activates_issue_tracker_spec.rb
index 9fc91e03c94..944a2c164d5 100644
--- a/spec/features/projects/integrations/user_activates_issue_tracker_spec.rb
+++ b/spec/features/projects/integrations/user_activates_issue_tracker_spec.rb
@@ -38,7 +38,8 @@ RSpec.describe 'User activates issue tracker', :js, feature_category: :integrati
end
it 'shows the link in the menu' do
- page.within('.nav-sidebar') do
+ within_testid('super-sidebar') do
+ click_button 'Plan'
expect(page).to have_link(tracker, href: url)
end
end
@@ -77,7 +78,8 @@ RSpec.describe 'User activates issue tracker', :js, feature_category: :integrati
end
it 'does not show the external tracker link in the menu' do
- page.within('.nav-sidebar') do
+ within_testid('super-sidebar') do
+ click_button 'Plan'
expect(page).not_to have_link(tracker, href: url)
end
end
diff --git a/spec/features/projects/integrations/user_activates_jira_spec.rb b/spec/features/projects/integrations/user_activates_jira_spec.rb
index 0bd5020e9bf..cc0d4c6f564 100644
--- a/spec/features/projects/integrations/user_activates_jira_spec.rb
+++ b/spec/features/projects/integrations/user_activates_jira_spec.rb
@@ -25,10 +25,11 @@ RSpec.describe 'User activates Jira', :js, feature_category: :integrations do
unless Gitlab.ee?
it 'adds Jira link to sidebar menu' do
- page.within('.nav-sidebar') do
- expect(page).not_to have_link('Jira issues', visible: false)
- expect(page).not_to have_link('Open Jira', href: url, visible: false)
- expect(page).to have_link('Jira', href: url)
+ within_testid('super-sidebar') do
+ click_button 'Plan'
+ expect(page).not_to have_link('Jira issues')
+ expect(page).not_to have_link('Open Jira')
+ expect(page).to have_link(exact_text: 'Jira', href: url)
end
end
end
@@ -76,8 +77,9 @@ RSpec.describe 'User activates Jira', :js, feature_category: :integrations do
end
it 'does not show the Jira link in the menu' do
- page.within('.nav-sidebar') do
- expect(page).not_to have_link('Jira', href: url)
+ within_testid('super-sidebar') do
+ click_button 'Plan'
+ expect(page).not_to have_link('Jira')
end
end
end
diff --git a/spec/features/projects/issuable_templates_spec.rb b/spec/features/projects/issuable_templates_spec.rb
index 9ba4b544191..bc67cdbfad1 100644
--- a/spec/features/projects/issuable_templates_spec.rb
+++ b/spec/features/projects/issuable_templates_spec.rb
@@ -64,6 +64,27 @@ RSpec.describe 'issuable templates', :js, feature_category: :groups_and_projects
end
end
+ context 'user creates an issue template using issuable_template query param' do
+ let(:template_content) { 'this is a test "bug" template' }
+
+ before do
+ project.repository.create_file(
+ user,
+ '.gitlab/issue_templates/bug.md',
+ template_content,
+ message: 'added issue template',
+ branch_name: 'master')
+ end
+
+ it 'applies correctly in the rich text editor' do
+ visit new_project_issue_path project
+ click_button "Switch to rich text editing"
+ visit new_project_issue_path(project, { issuable_template: 'bug' })
+
+ expect(page).to have_content(template_content)
+ end
+ end
+
context 'user creates an issue using templates, with a prior description' do
let(:prior_description) { 'test issue description' }
let(:template_content) { 'this is a test "bug" template' }
diff --git a/spec/features/projects/issues/email_participants_spec.rb b/spec/features/projects/issues/email_participants_spec.rb
index 215c45351c1..e1b8133a10f 100644
--- a/spec/features/projects/issues/email_participants_spec.rb
+++ b/spec/features/projects/issues/email_participants_spec.rb
@@ -68,18 +68,4 @@ RSpec.describe 'viewing an issue', :js, feature_category: :service_desk do
end
end
end
-
- context 'for feature flags' do
- before do
- sign_in(user)
- end
-
- it 'pushes service_desk_new_note_email_native_attachments feature flag to frontend' do
- stub_feature_flags(service_desk_new_note_email_native_attachments: true)
-
- visit project_issue_path(project, issue)
-
- expect(page).to have_pushed_frontend_feature_flags(serviceDeskNewNoteEmailNativeAttachments: true)
- end
- end
end
diff --git a/spec/features/projects/jobs/user_browses_jobs_spec.rb b/spec/features/projects/jobs/user_browses_jobs_spec.rb
index fc67d7dedcc..115b3dda5b2 100644
--- a/spec/features/projects/jobs/user_browses_jobs_spec.rb
+++ b/spec/features/projects/jobs/user_browses_jobs_spec.rb
@@ -72,7 +72,7 @@ RSpec.describe 'User browses jobs', feature_category: :groups_and_projects do
wait_for_requests
- expect(page).to have_selector('[data-testid="ci-badge-link"]', text: 'Canceled')
+ expect(page).to have_selector('[data-testid="ci-icon"]', text: 'Canceled')
expect(page).not_to have_selector('[data-testid="jobs-table-error-alert"]')
end
end
@@ -93,7 +93,7 @@ RSpec.describe 'User browses jobs', feature_category: :groups_and_projects do
wait_for_requests
- expect(page).to have_selector('[data-testid="ci-badge-link"]', text: 'Pending')
+ expect(page).to have_selector('[data-testid="ci-icon"]', text: 'Pending')
end
end
@@ -133,7 +133,7 @@ RSpec.describe 'User browses jobs', feature_category: :groups_and_projects do
wait_for_requests
- expect(page).to have_selector('[data-testid="ci-badge-link"]', text: 'Pending')
+ expect(page).to have_selector('[data-testid="ci-icon"]', text: 'Pending')
end
it 'unschedules a job successfully' do
@@ -141,7 +141,7 @@ RSpec.describe 'User browses jobs', feature_category: :groups_and_projects do
wait_for_requests
- expect(page).to have_selector('[data-testid="ci-badge-link"]', text: 'Manual')
+ expect(page).to have_selector('[data-testid="ci-icon"]', text: 'Manual')
end
end
diff --git a/spec/features/projects/jobs_spec.rb b/spec/features/projects/jobs_spec.rb
index 12ed2558712..050ed4e0e4c 100644
--- a/spec/features/projects/jobs_spec.rb
+++ b/spec/features/projects/jobs_spec.rb
@@ -66,7 +66,7 @@ RSpec.describe 'Jobs', :clean_gitlab_redis_shared_state, feature_category: :grou
wait_for_requests
- expect(page).to have_selector('[data-testid="ci-badge-link"]', text: 'Passed')
+ expect(page).to have_selector('[data-testid="ci-icon"]', text: 'Passed')
end
it 'shows commit`s data', :js do
diff --git a/spec/features/projects/members/sorting_spec.rb b/spec/features/projects/members/sorting_spec.rb
index 9747d499ae9..94c42c0f098 100644
--- a/spec/features/projects/members/sorting_spec.rb
+++ b/spec/features/projects/members/sorting_spec.rb
@@ -149,7 +149,7 @@ RSpec.describe 'Projects > Members > Sorting', :js, feature_category: :groups_an
def expect_sort_by(text, sort_direction)
within('[data-testid="members-sort-dropdown"]') do
expect(page).to have_css('button[aria-haspopup="menu"]', text: text)
- expect(page).to have_button("Sorting Direction: #{sort_direction == :asc ? 'Ascending' : 'Descending'}")
+ expect(page).to have_button("Sort direction: #{sort_direction == :asc ? 'Ascending' : 'Descending'}")
end
end
end
diff --git a/spec/features/projects/members/user_requests_access_spec.rb b/spec/features/projects/members/user_requests_access_spec.rb
index d1e58ba91f0..e7f99a4048c 100644
--- a/spec/features/projects/members/user_requests_access_spec.rb
+++ b/spec/features/projects/members/user_requests_access_spec.rb
@@ -5,8 +5,8 @@ require 'spec_helper'
RSpec.describe 'Projects > Members > User requests access', :js, feature_category: :groups_and_projects do
include Spec::Support::Helpers::ModalHelpers
- let_it_be(:user) { create(:user, :no_super_sidebar) }
- let_it_be(:maintainer) { create(:user, :no_super_sidebar) }
+ let_it_be(:user) { create(:user) }
+ let_it_be(:maintainer) { create(:user) }
let_it_be(:project) { create(:project, :public, :repository) }
let(:owner) { project.first_owner }
@@ -54,10 +54,9 @@ RSpec.describe 'Projects > Members > User requests access', :js, feature_categor
expect(project.requesters.exists?(user_id: user)).to be_truthy
- click_link 'Project information'
-
- page.within('.nav-sidebar') do
- click_link('Members')
+ within_testid('super-sidebar') do
+ click_button 'Manage'
+ click_link 'Members'
end
page.within('.content') do
diff --git a/spec/features/projects/milestones/user_interacts_with_labels_spec.rb b/spec/features/projects/milestones/user_interacts_with_labels_spec.rb
index 3742c9f19d8..9c3eaff1545 100644
--- a/spec/features/projects/milestones/user_interacts_with_labels_spec.rb
+++ b/spec/features/projects/milestones/user_interacts_with_labels_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe 'User interacts with labels', feature_category: :team_planning do
- let(:user) { create(:user, :no_super_sidebar) }
+ let(:user) { create(:user) }
let(:project) { create(:project, namespace: user.namespace) }
let(:milestone) { create(:milestone, project: project, title: 'v2.2', description: '# Description header') }
let(:issue1) { create(:issue, project: project, title: 'Bugfix1', milestone: milestone) }
@@ -25,9 +25,7 @@ RSpec.describe 'User interacts with labels', feature_category: :team_planning do
it 'shows the list of labels', :js do
click_link('v2.2')
- page.within('.nav-sidebar') do
- page.find(:xpath, "//a[@href='#tab-labels']").click
- end
+ page.find(:xpath, "//a[@href='#tab-labels']").click
expect(page).to have_selector('ul.manage-labels-list')
diff --git a/spec/features/projects/navbar_spec.rb b/spec/features/projects/navbar_spec.rb
index e967c1be3bc..348a661855c 100644
--- a/spec/features/projects/navbar_spec.rb
+++ b/spec/features/projects/navbar_spec.rb
@@ -2,13 +2,13 @@
require 'spec_helper'
-RSpec.describe 'Project navbar', :with_license, feature_category: :groups_and_projects do
+RSpec.describe 'Project navbar', :with_license, :js, feature_category: :groups_and_projects do
include NavbarStructureHelper
include WaitForRequests
include_context 'project navbar structure'
- let_it_be(:user) { create(:user, :no_super_sidebar) }
+ let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project, :repository, namespace: user.namespace) }
before do
@@ -16,7 +16,7 @@ RSpec.describe 'Project navbar', :with_license, feature_category: :groups_and_pr
stub_config(registry: { enabled: false })
stub_feature_flags(ml_experiment_tracking: false)
- insert_package_nav(_('Deployments'))
+ insert_package_nav
insert_infrastructure_registry_nav
insert_infrastructure_google_cloud_nav
insert_infrastructure_aws_nav
@@ -28,29 +28,13 @@ RSpec.describe 'Project navbar', :with_license, feature_category: :groups_and_pr
end
end
- context 'when value stream is available' do
- before do
- visit project_path(project)
- end
-
- it 'redirects to value stream when Analytics item is clicked' do
- page.within('.sidebar-top-level-items') do
- find('.shortcuts-analytics').click
- end
-
- wait_for_requests
-
- expect(page).to have_current_path(project_cycle_analytics_path(project))
- end
- end
-
context 'when pages are available' do
before do
stub_config(pages: { enabled: true })
insert_after_sub_nav_item(
- _('Releases'),
- within: _('Deployments'),
+ _('Package Registry'),
+ within: _('Deploy'),
new_sub_nav_item_name: _('Pages')
)
@@ -86,7 +70,7 @@ RSpec.describe 'Project navbar', :with_license, feature_category: :groups_and_pr
let_it_be(:harbor_integration) { create(:harbor_integration, project: project) }
before do
- insert_harbor_registry_nav(_('Terraform modules'))
+ insert_harbor_registry_nav(_('AWS'))
visit project_path(project)
end
@@ -98,7 +82,11 @@ RSpec.describe 'Project navbar', :with_license, feature_category: :groups_and_pr
before do
stub_feature_flags(ml_experiment_tracking: true)
- insert_model_experiments_nav(_('Terraform modules'))
+ if Gitlab.ee? # rubocop: disable RSpec/AvoidConditionalStatements
+ insert_model_experiments_nav(_('Merge request analytics'))
+ else
+ insert_model_experiments_nav(_('Repository analytics'))
+ end
visit project_path(project)
end
diff --git a/spec/features/projects/network_graph_spec.rb b/spec/features/projects/network_graph_spec.rb
index eff0335c891..e84bbf382ad 100644
--- a/spec/features/projects/network_graph_spec.rb
+++ b/spec/features/projects/network_graph_spec.rb
@@ -124,12 +124,4 @@ RSpec.describe 'Project Network Graph', :js, feature_category: :groups_and_proje
end
it_behaves_like 'network graph'
-
- context 'when disable_network_graph_notes_count is disabled' do
- before do
- stub_feature_flags(disable_network_graph_notes_count: false)
- end
-
- it_behaves_like 'network graph'
- end
end
diff --git a/spec/features/projects/new_project_spec.rb b/spec/features/projects/new_project_spec.rb
index 926fea24e14..a3cbb86da2c 100644
--- a/spec/features/projects/new_project_spec.rb
+++ b/spec/features/projects/new_project_spec.rb
@@ -9,13 +9,37 @@ RSpec.describe 'New project', :js, feature_category: :groups_and_projects do
stub_application_setting(import_sources: Gitlab::ImportSources.values)
end
+ shared_examples 'shows correct navigation' do
+ context 'for a new top-level project' do
+ it 'shows the "Your work" navigation' do
+ visit new_project_path
+ expect(page).to have_selector(".super-sidebar", text: "Your work")
+ end
+ end
+
+ context 'for a new group project' do
+ let_it_be(:parent_group) { create(:group) }
+
+ before do
+ parent_group.add_owner(user)
+ end
+
+ it 'shows the group sidebar of the parent group' do
+ visit new_project_path(namespace_id: parent_group.id)
+ expect(page).to have_selector(".super-sidebar", text: parent_group.name)
+ end
+ end
+ end
+
context 'as a user' do
- let_it_be(:user) { create(:user, :no_super_sidebar) }
+ let_it_be(:user) { create(:user) }
before do
sign_in(user)
end
+ it_behaves_like 'shows correct navigation'
+
it 'shows the project description field when it should' do
description_label = 'Project description (optional)'
@@ -76,7 +100,9 @@ RSpec.describe 'New project', :js, feature_category: :groups_and_projects do
end
context 'as an admin' do
- let(:user) { create(:admin, :no_super_sidebar) }
+ let(:user) { create(:admin) }
+
+ it_behaves_like 'shows correct navigation'
shared_examples '"New project" page' do
before do
@@ -103,14 +129,6 @@ RSpec.describe 'New project', :js, feature_category: :groups_and_projects do
include_examples '"New project" page'
- context 'when the new navigation is enabled' do
- before do
- user.update!(use_new_navigation: true)
- end
-
- include_examples '"New project" page'
- end
-
shared_examples 'renders importer link' do |params|
context 'with user namespace' do
before do
@@ -566,66 +584,17 @@ RSpec.describe 'New project', :js, feature_category: :groups_and_projects do
let(:provider) { :bitbucket }
context 'as a user' do
- let(:user) { create(:user, :no_super_sidebar) }
+ let(:user) { create(:user) }
let(:oauth_config_instructions) { 'To enable importing projects from Bitbucket, ask your GitLab administrator to configure OAuth integration' }
it_behaves_like 'has instructions to enable OAuth'
end
context 'as an admin', :do_not_mock_admin_mode_setting do
- let(:user) { create(:admin, :no_super_sidebar) }
+ let(:user) { create(:admin) }
let(:oauth_config_instructions) { 'To enable importing projects from Bitbucket, as administrator you need to configure OAuth integration' }
it_behaves_like 'has instructions to enable OAuth'
end
end
-
- describe 'sidebar' do
- let_it_be(:user) { create(:user, :no_super_sidebar) }
- let_it_be(:parent_group) { create(:group) }
-
- before do
- parent_group.add_owner(user)
- sign_in(user)
- end
-
- context 'in the current navigation' do
- before do
- user.update!(use_new_navigation: false)
- end
-
- context 'for a new top-level project' do
- it_behaves_like 'a "Your work" page with sidebar and breadcrumbs', :new_project_path, :projects
- end
-
- context 'for a new group project' do
- it 'shows the group sidebar of the parent group' do
- visit new_project_path(namespace_id: parent_group.id)
- expect(page).to have_selector(".nav-sidebar[aria-label=\"Group navigation\"] .context-header[title=\"#{parent_group.name}\"]")
- end
- end
- end
-
- context 'in the new navigation' do
- before do
- parent_group.add_owner(user)
- user.update!(use_new_navigation: true)
- sign_in(user)
- end
-
- context 'for a new top-level project' do
- it 'shows the "Your work" navigation' do
- visit new_project_path
- expect(page).to have_selector(".super-sidebar", text: "Your work")
- end
- end
-
- context 'for a new group project' do
- it 'shows the group sidebar of the parent group' do
- visit new_project_path(namespace_id: parent_group.id)
- expect(page).to have_selector(".super-sidebar", text: parent_group.name)
- end
- end
- end
- end
end
diff --git a/spec/features/projects/pages/user_configures_pages_pipeline_spec.rb b/spec/features/projects/pages/user_configures_pages_pipeline_spec.rb
index baef75ca303..eb7bcb38d38 100644
--- a/spec/features/projects/pages/user_configures_pages_pipeline_spec.rb
+++ b/spec/features/projects/pages/user_configures_pages_pipeline_spec.rb
@@ -15,45 +15,23 @@ RSpec.describe 'Pages edits pages settings', :js, feature_category: :pages do
sign_in(user)
end
- context 'when pipeline wizard feature is enabled' do
- before do
- Feature.enable(:use_pipeline_wizard_for_pages)
- end
-
- context 'when onboarding is not complete' do
- it 'renders onboarding instructions' do
- visit project_pages_path(project)
-
- expect(page).to have_content('Get started with Pages')
- end
- end
-
- context 'when onboarding is complete' do
- before do
- project.mark_pages_onboarding_complete
- end
-
- it 'shows waiting screen' do
- visit project_pages_path(project)
+ context 'when onboarding is not complete' do
+ it 'renders onboarding instructions' do
+ visit project_pages_path(project)
- expect(page).to have_content('Waiting for the Pages Pipeline to complete...')
- end
+ expect(page).to have_content('Get started with GitLab Pages')
end
end
- context 'when pipeline wizard feature is disabled' do
+ context 'when onboarding is complete' do
before do
- Feature.disable(:use_pipeline_wizard_for_pages)
- end
-
- after do
- Feature.enable(:use_pipeline_wizard_for_pages)
+ project.mark_pages_onboarding_complete
end
- it 'shows configure pages instructions' do
+ it 'shows waiting screen' do
visit project_pages_path(project)
- expect(page).to have_content('Configure pages')
+ expect(page).to have_content('Waiting for the Pages Pipeline to complete...')
end
end
end
diff --git a/spec/features/projects/pages/user_edits_settings_spec.rb b/spec/features/projects/pages/user_edits_settings_spec.rb
index 8350214bf99..4ad729a29e1 100644
--- a/spec/features/projects/pages/user_edits_settings_spec.rb
+++ b/spec/features/projects/pages/user_edits_settings_spec.rb
@@ -5,7 +5,7 @@ RSpec.describe 'Pages edits pages settings', :js, feature_category: :pages do
include Spec::Support::Helpers::ModalHelpers
let_it_be_with_reload(:project) { create(:project, :pages_published, pages_https_only: false) }
- let_it_be(:user) { create(:user, :no_super_sidebar) }
+ let_it_be(:user) { create(:user) }
before do
allow(Gitlab.config.pages).to receive(:enabled).and_return(true)
@@ -22,7 +22,7 @@ RSpec.describe 'Pages edits pages settings', :js, feature_category: :pages do
context 'when pages deployed' do
before do
- project.mark_pages_as_deployed
+ create(:pages_deployment, project: project)
end
it 'renders Access pages' do
@@ -85,7 +85,7 @@ RSpec.describe 'Pages edits pages settings', :js, feature_category: :pages do
it 'renders "Pages" tab' do
visit project_pages_path(project)
- page.within '.nav-sidebar' do
+ within_testid 'super-sidebar' do
expect(page).to have_link('Pages')
end
end
@@ -96,7 +96,8 @@ RSpec.describe 'Pages edits pages settings', :js, feature_category: :pages do
it 'renders "Pages" tab' do
visit project_environments_path(project)
- page.within '.nav-sidebar' do
+ within_testid 'super-sidebar' do
+ click_button 'Deploy'
expect(page).to have_link('Pages')
end
end
@@ -110,7 +111,8 @@ RSpec.describe 'Pages edits pages settings', :js, feature_category: :pages do
it 'does not render "Pages" tab' do
visit project_environments_path(project)
- page.within '.nav-sidebar' do
+ within_testid 'super-sidebar' do
+ click_button 'Deploy'
expect(page).not_to have_link('Pages')
end
end
@@ -123,7 +125,7 @@ RSpec.describe 'Pages edits pages settings', :js, feature_category: :pages do
before do
project.namespace.update!(owner: user)
- project.mark_pages_as_deployed
+ create(:pages_deployment, project: project)
end
it 'tries to change the setting' do
@@ -185,7 +187,7 @@ RSpec.describe 'Pages edits pages settings', :js, feature_category: :pages do
describe 'Remove page' do
context 'when pages are deployed' do
before do
- project.mark_pages_as_deployed
+ create(:pages_deployment, project: project)
end
it 'removes the pages', :sidekiq_inline do
diff --git a/spec/features/projects/pipelines/pipeline_spec.rb b/spec/features/projects/pipelines/pipeline_spec.rb
index f042a12884c..fccfe00f593 100644
--- a/spec/features/projects/pipelines/pipeline_spec.rb
+++ b/spec/features/projects/pipelines/pipeline_spec.rb
@@ -165,7 +165,7 @@ RSpec.describe 'Pipeline', :js, feature_category: :continuous_integration do
it 'shows a running icon and a cancel action for the running build' do
page.within('#ci-badge-deploy') do
- expect(page).to have_selector('[data-testid="status_running-icon"]')
+ expect(page).to have_selector('[data-testid="status_running_borderless-icon"]')
expect(page).to have_selector('.js-icon-cancel')
expect(page).to have_content('deploy')
end
@@ -187,7 +187,7 @@ RSpec.describe 'Pipeline', :js, feature_category: :continuous_integration do
it 'shows a preparing icon and a cancel action' do
page.within('#ci-badge-prepare') do
- expect(page).to have_selector('[data-testid="status_preparing-icon"]')
+ expect(page).to have_selector('[data-testid="status_preparing_borderless-icon"]')
expect(page).to have_selector('.js-icon-cancel')
expect(page).to have_content('prepare')
end
@@ -209,7 +209,7 @@ RSpec.describe 'Pipeline', :js, feature_category: :continuous_integration do
it 'shows the success icon and a retry action for the successful build' do
page.within('#ci-badge-build') do
- expect(page).to have_selector('[data-testid="status_success-icon"]')
+ expect(page).to have_selector('[data-testid="status_success_borderless-icon"]')
expect(page).to have_content('build')
end
@@ -224,7 +224,7 @@ RSpec.describe 'Pipeline', :js, feature_category: :continuous_integration do
expect(page).not_to have_content('Retry job')
within('[data-testid="pipeline-details-header"]') do
- expect(page).to have_selector('[data-testid="ci-badge-link"]', text: 'Running')
+ expect(page).to have_selector('[data-testid="ci-icon"]', text: 'Running')
end
end
end
@@ -238,7 +238,7 @@ RSpec.describe 'Pipeline', :js, feature_category: :continuous_integration do
it 'shows the scheduled icon and an unschedule action for the delayed job' do
page.within('#ci-badge-delayed-job') do
- expect(page).to have_selector('[data-testid="status_scheduled-icon"]')
+ expect(page).to have_selector('[data-testid="status_scheduled_borderless-icon"]')
expect(page).to have_content('delayed-job')
end
@@ -263,7 +263,7 @@ RSpec.describe 'Pipeline', :js, feature_category: :continuous_integration do
it 'shows the failed icon and a retry action for the failed build' do
page.within('#ci-badge-test') do
- expect(page).to have_selector('[data-testid="status_failed-icon"]')
+ expect(page).to have_selector('[data-testid="status_failed_borderless-icon"]')
expect(page).to have_content('test')
end
@@ -278,7 +278,7 @@ RSpec.describe 'Pipeline', :js, feature_category: :continuous_integration do
expect(page).not_to have_content('Retry job')
within('[data-testid="pipeline-details-header"]') do
- expect(page).to have_selector('[data-testid="ci-badge-link"]', text: 'Running')
+ expect(page).to have_selector('[data-testid="ci-icon"]', text: 'Running')
end
end
@@ -297,7 +297,7 @@ RSpec.describe 'Pipeline', :js, feature_category: :continuous_integration do
it 'shows the skipped icon and a play action for the manual build' do
page.within('#ci-badge-manual-build') do
- expect(page).to have_selector('[data-testid="status_manual-icon"]')
+ expect(page).to have_selector('[data-testid="status_manual_borderless-icon"]')
expect(page).to have_content('manual')
end
@@ -312,7 +312,7 @@ RSpec.describe 'Pipeline', :js, feature_category: :continuous_integration do
expect(page).not_to have_content('Play job')
within('[data-testid="pipeline-details-header"]') do
- expect(page).to have_selector('[data-testid="ci-badge-link"]', text: 'Running')
+ expect(page).to have_selector('[data-testid="ci-icon"]', text: 'Running')
end
end
end
@@ -323,7 +323,7 @@ RSpec.describe 'Pipeline', :js, feature_category: :continuous_integration do
end
it 'shows the success icon and the generic comit status build' do
- expect(page).to have_selector('[data-testid="status_success-icon"]')
+ expect(page).to have_selector('[data-testid="status_success_borderless-icon"]')
expect(page).to have_content('jenkins')
expect(page).to have_link('jenkins', href: 'http://gitlab.com/status')
end
@@ -358,7 +358,7 @@ RSpec.describe 'Pipeline', :js, feature_category: :continuous_integration do
let(:status) { :success }
it 'does not show the cancel or retry action' do
- expect(page).to have_selector('.ci-status-icon-success')
+ expect(page).to have_selector('[data-testid="status_success_borderless-icon"]')
expect(page).not_to have_selector('button[aria-label="Retry downstream pipeline"]')
expect(page).not_to have_selector('button[aria-label="Cancel downstream pipeline"]')
end
@@ -379,7 +379,7 @@ RSpec.describe 'Pipeline', :js, feature_category: :continuous_integration do
it 'shows the pipeline as canceled with the retry action' do
expect(page).to have_selector('button[aria-label="Retry downstream pipeline"]')
- expect(page).to have_selector('.ci-status-icon-canceled')
+ expect(page).to have_selector('[data-testid="status_canceled_borderless-icon"]')
end
end
end
@@ -398,7 +398,7 @@ RSpec.describe 'Pipeline', :js, feature_category: :continuous_integration do
end
it 'shows running pipeline with the cancel action' do
- expect(page).to have_selector('.ci-status-icon-running')
+ expect(page).to have_selector('[data-testid="status_running_borderless-icon"]')
expect(page).to have_selector('button[aria-label="Cancel downstream pipeline"]')
end
end
@@ -418,7 +418,7 @@ RSpec.describe 'Pipeline', :js, feature_category: :continuous_integration do
end
it 'shows running pipeline with the cancel action' do
- expect(page).to have_selector('.ci-status-icon-running')
+ expect(page).to have_selector('[data-testid="status_running_borderless-icon"]')
expect(page).to have_selector('button[aria-label="Cancel downstream pipeline"]')
end
end
@@ -438,7 +438,7 @@ RSpec.describe 'Pipeline', :js, feature_category: :continuous_integration do
end
it 'does not show the retry button' do
- expect(page).to have_selector('.ci-status-icon-failed')
+ expect(page).to have_selector('[data-testid="status_failed_borderless-icon"]')
expect(page).not_to have_selector('button[aria-label="Retry downstream pipeline"]')
end
end
@@ -537,7 +537,7 @@ RSpec.describe 'Pipeline', :js, feature_category: :continuous_integration do
it 'shows running status in pipeline header', :sidekiq_might_not_need_inline do
within('[data-testid="pipeline-details-header"]') do
- expect(page).to have_selector('[data-testid="ci-badge-link"]', text: 'Running')
+ expect(page).to have_selector('[data-testid="ci-icon"]', text: 'Running')
end
end
end
@@ -782,8 +782,8 @@ RSpec.describe 'Pipeline', :js, feature_category: :continuous_integration do
expect(page).to have_content('Cancel pipeline')
end
- it 'does not link to job', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/408215' do
- expect(page).not_to have_selector('.js-pipeline-graph-job-link')
+ it 'does link to job' do
+ expect(page).to have_selector('.js-pipeline-graph-job-link')
end
end
end
@@ -900,18 +900,18 @@ RSpec.describe 'Pipeline', :js, feature_category: :continuous_integration do
subject
within('[data-testid="pipeline-details-header"]') do
- expect(page).to have_selector('[data-testid="ci-badge-link"]', text: 'Pending')
+ expect(page).to have_selector('[data-testid="ci-icon"]', text: 'Pending')
end
within('.js-pipeline-graph') do
within(all('[data-testid="stage-column"]')[0]) do
expect(page).to have_content('test')
- expect(page).to have_css('.ci-status-icon-pending')
+ expect(page).to have_css('[data-testid="status_pending_borderless-icon"]')
end
within(all('[data-testid="stage-column"]')[1]) do
expect(page).to have_content('deploy')
- expect(page).to have_css('.ci-status-icon-created')
+ expect(page).to have_css('[data-testid="status_created_borderless-icon"]')
end
end
end
@@ -925,18 +925,18 @@ RSpec.describe 'Pipeline', :js, feature_category: :continuous_integration do
subject
within('[data-testid="pipeline-details-header"]') do
- expect(page).to have_selector('[data-testid="ci-badge-link"]', text: 'Running')
+ expect(page).to have_selector('[data-testid="ci-icon"]', text: 'Running')
end
within('.js-pipeline-graph') do
within(all('[data-testid="stage-column"]')[0]) do
expect(page).to have_content('test')
- expect(page).to have_css('.ci-status-icon-success')
+ expect(page).to have_css('[data-testid="status_success_borderless-icon"]')
end
within(all('[data-testid="stage-column"]')[1]) do
expect(page).to have_content('deploy')
- expect(page).to have_css('.ci-status-icon-pending')
+ expect(page).to have_css('[data-testid="status_pending_borderless-icon"]')
end
end
end
@@ -954,13 +954,13 @@ RSpec.describe 'Pipeline', :js, feature_category: :continuous_integration do
subject
within('[data-testid="pipeline-details-header"]') do
- expect(page).to have_selector('[data-testid="ci-badge-link"]', text: 'Waiting')
+ expect(page).to have_selector('[data-testid="ci-icon"]', text: 'Waiting')
end
within('.js-pipeline-graph') do
within(all('[data-testid="stage-column"]')[1]) do
expect(page).to have_content('deploy')
- expect(page).to have_css('.ci-status-icon-waiting-for-resource')
+ expect(page).to have_css('[data-testid="status_pending_borderless-icon"]')
end
end
end
@@ -974,13 +974,13 @@ RSpec.describe 'Pipeline', :js, feature_category: :continuous_integration do
subject
within('[data-testid="pipeline-details-header"]') do
- expect(page).to have_selector('[data-testid="ci-badge-link"]', text: 'Running')
+ expect(page).to have_selector('[data-testid="ci-icon"]', text: 'Running')
end
within('.js-pipeline-graph') do
within(all('[data-testid="stage-column"]')[1]) do
expect(page).to have_content('deploy')
- expect(page).to have_css('.ci-status-icon-pending')
+ expect(page).to have_css('[data-testid="status_pending_borderless-icon"]')
end
end
end
@@ -1002,13 +1002,13 @@ RSpec.describe 'Pipeline', :js, feature_category: :continuous_integration do
subject
within('[data-testid="pipeline-details-header"]') do
- expect(page).to have_selector('[data-testid="ci-badge-link"]', text: 'Waiting')
+ expect(page).to have_selector('[data-testid="ci-icon"]', text: 'Waiting')
end
within('.js-pipeline-graph') do
within(all('[data-testid="stage-column"]')[1]) do
expect(page).to have_content('deploy')
- expect(page).to have_css('.ci-status-icon-waiting-for-resource')
+ expect(page).to have_css('[data-testid="status_pending_borderless-icon"]')
end
end
end
diff --git a/spec/features/projects/pipelines/pipelines_spec.rb b/spec/features/projects/pipelines/pipelines_spec.rb
index ca3b7f0ad47..30d3303dfbb 100644
--- a/spec/features/projects/pipelines/pipelines_spec.rb
+++ b/spec/features/projects/pipelines/pipelines_spec.rb
@@ -11,7 +11,7 @@ RSpec.describe 'Pipelines', :js, feature_category: :continuous_integration do
let(:expected_detached_mr_tag) { 'merge request' }
context 'when user is logged in' do
- let(:user) { create(:user, :no_super_sidebar) }
+ let(:user) { create(:user) }
before do
sign_in(user)
@@ -115,7 +115,7 @@ RSpec.describe 'Pipelines', :js, feature_category: :continuous_integration do
it 'indicates that pipeline can be canceled' do
expect(page).to have_selector('.js-pipelines-cancel-button')
- expect(page).to have_selector('[data-testid="ci-badge-link"]', text: 'Running')
+ expect(page).to have_selector('[data-testid="ci-icon"]', text: 'Running')
end
context 'when canceling' do
@@ -127,7 +127,7 @@ RSpec.describe 'Pipelines', :js, feature_category: :continuous_integration do
it 'indicated that pipelines was canceled', :sidekiq_might_not_need_inline do
expect(page).not_to have_selector('.js-pipelines-cancel-button')
- expect(page).to have_selector('[data-testid="ci-badge-link"]', text: 'Canceled')
+ expect(page).to have_selector('[data-testid="ci-icon"]', text: 'Canceled')
end
end
end
@@ -144,7 +144,7 @@ RSpec.describe 'Pipelines', :js, feature_category: :continuous_integration do
it 'indicates that pipeline can be retried' do
expect(page).to have_selector('.js-pipelines-retry-button')
- expect(page).to have_selector('[data-testid="ci-badge-link"]', text: 'Failed')
+ expect(page).to have_selector('[data-testid="ci-icon"]', text: 'Failed')
end
context 'when retrying' do
@@ -155,7 +155,7 @@ RSpec.describe 'Pipelines', :js, feature_category: :continuous_integration do
it 'shows running pipeline that is not retryable' do
expect(page).not_to have_selector('.js-pipelines-retry-button')
- expect(page).to have_selector('[data-testid="ci-badge-link"]', text: 'Running')
+ expect(page).to have_selector('[data-testid="ci-icon"]', text: 'Running')
end
end
end
@@ -183,7 +183,7 @@ RSpec.describe 'Pipelines', :js, feature_category: :continuous_integration do
within '.pipeline-tags' do
expect(page).to have_content(expected_detached_mr_tag)
- expect(page).to have_link(merge_request.iid, href: project_merge_request_path(project, merge_request))
+ expect(page).to have_link(merge_request.iid.to_s, href: project_merge_request_path(project, merge_request))
expect(page).not_to have_link(pipeline.ref)
end
@@ -223,7 +223,7 @@ RSpec.describe 'Pipelines', :js, feature_category: :continuous_integration do
within '.pipeline-tags' do
expect(page).not_to have_content(expected_detached_mr_tag)
- expect(page).to have_link(merge_request.iid, href: project_merge_request_path(project, merge_request))
+ expect(page).to have_link(merge_request.iid.to_s, href: project_merge_request_path(project, merge_request))
expect(page).not_to have_link(pipeline.ref)
end
@@ -396,7 +396,7 @@ RSpec.describe 'Pipelines', :js, feature_category: :continuous_integration do
end
it 'shows the pipeline as preparing' do
- expect(page).to have_selector('[data-testid="ci-badge-link"]', text: 'Preparing')
+ expect(page).to have_selector('[data-testid="ci-icon"]', text: 'Preparing')
end
end
@@ -417,7 +417,7 @@ RSpec.describe 'Pipelines', :js, feature_category: :continuous_integration do
end
it 'has pipeline running' do
- expect(page).to have_selector('[data-testid="ci-badge-link"]', text: 'Running')
+ expect(page).to have_selector('[data-testid="ci-icon"]', text: 'Running')
end
context 'when canceling' do
@@ -428,7 +428,7 @@ RSpec.describe 'Pipelines', :js, feature_category: :continuous_integration do
it 'indicates that pipeline was canceled', :sidekiq_might_not_need_inline do
expect(page).not_to have_selector('.js-pipelines-cancel-button')
- expect(page).to have_selector('[data-testid="ci-badge-link"]', text: 'Canceled')
+ expect(page).to have_selector('[data-testid="ci-icon"]', text: 'Canceled')
end
end
end
@@ -450,7 +450,7 @@ RSpec.describe 'Pipelines', :js, feature_category: :continuous_integration do
end
it 'has failed pipeline', :sidekiq_might_not_need_inline do
- expect(page).to have_selector('[data-testid="ci-badge-link"]', text: 'Failed')
+ expect(page).to have_selector('[data-testid="ci-icon"]', text: 'Failed')
end
end
end
@@ -650,7 +650,7 @@ RSpec.describe 'Pipelines', :js, feature_category: :continuous_integration do
# header
expect(page).to have_text("##{pipeline.id}")
- expect(page).to have_link(pipeline.user.name, href: user_path(pipeline.user))
+ expect(page).to have_link(pipeline.user.name, href: /#{user_path(pipeline.user)}$/)
# stages
expect(page).to have_text('build')
@@ -805,29 +805,12 @@ RSpec.describe 'Pipelines', :js, feature_category: :continuous_integration do
describe 'Empty State' do
let(:project) { create(:project, :repository) }
- context 'when `ios_specific_templates` is not enabled' do
- before do
- visit project_pipelines_path(project)
- end
-
- it 'renders empty state' do
- expect(page).to have_content 'Try test template'
- end
+ before do
+ visit project_pipelines_path(project)
end
- describe 'when the `ios_specific_templates` experiment is enabled and the "Set up a runner" button is clicked' do
- before do
- stub_experiments(ios_specific_templates: :candidate)
- project.project_setting.update!(target_platforms: %w[ios])
- visit project_pipelines_path(project)
- click_button 'Set up a runner'
- end
-
- it 'displays a modal with the macOS platform selected and an explanation popover' do
- expect(page).to have_button 'macOS', class: 'selected'
- expect(page).to have_selector('#runner-instructions-modal___BV_modal_content_')
- expect(page).to have_selector('.popover')
- end
+ it 'renders empty state' do
+ expect(page).to have_content 'Try test template'
end
end
end
diff --git a/spec/features/projects/releases/user_creates_release_spec.rb b/spec/features/projects/releases/user_creates_release_spec.rb
index 678c8df666f..319053ddcb5 100644
--- a/spec/features/projects/releases/user_creates_release_spec.rb
+++ b/spec/features/projects/releases/user_creates_release_spec.rb
@@ -148,8 +148,7 @@ RSpec.describe 'User creates release', :js, feature_category: :continuous_delive
fill_release_title(release_title)
- select_milestone(milestone_1.title)
- select_milestone(milestone_2.title)
+ select_milestones(milestone_1.title, milestone_2.title)
fill_release_notes(release_notes)
diff --git a/spec/features/projects/releases/user_views_edit_release_spec.rb b/spec/features/projects/releases/user_views_edit_release_spec.rb
index b3f21a2d328..203b4ce82e3 100644
--- a/spec/features/projects/releases/user_views_edit_release_spec.rb
+++ b/spec/features/projects/releases/user_views_edit_release_spec.rb
@@ -20,8 +20,8 @@ RSpec.describe 'User edits Release', :js, feature_category: :continuous_delivery
end
def fill_out_form_and_click(button_to_click)
- fill_in 'Release title', with: 'Updated Release title'
- fill_in 'Release notes', with: 'Updated Release notes'
+ fill_in 'release-title', with: 'Updated Release title', fill_options: { clear: :backspace }
+ fill_in 'release-notes', with: 'Updated Release notes'
click_link_or_button button_to_click
@@ -44,8 +44,8 @@ RSpec.describe 'User edits Release', :js, feature_category: :continuous_delivery
expect(page).to have_content('Releases are based on Git tags. We recommend tags that use semantic versioning, for example 1.0.0, 2.1.0-pre.')
expect(find_field('Tag name', disabled: true).value).to eq(release.tag)
- expect(find_field('Release title').value).to eq(release.name)
- expect(find_field('Release notes').value).to eq(release.description)
+ expect(find_field('release-title').value).to eq(release.name)
+ expect(find_field('release-notes').value).to eq(release.description)
expect(page).to have_button('Save changes')
expect(page).to have_link('Cancel')
diff --git a/spec/features/projects/settings/monitor_settings_spec.rb b/spec/features/projects/settings/monitor_settings_spec.rb
index c2914c020e3..fca10d9c0b0 100644
--- a/spec/features/projects/settings/monitor_settings_spec.rb
+++ b/spec/features/projects/settings/monitor_settings_spec.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe 'Projects > Settings > For a forked project', :js, feature_category: :groups_and_projects do
include ListboxHelpers
- let_it_be(:user) { create(:user, :no_super_sidebar) }
+ let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project, :repository, create_templates: :issue, namespace: user.namespace) }
before do
@@ -15,13 +15,10 @@ RSpec.describe 'Projects > Settings > For a forked project', :js, feature_catego
describe 'Sidebar > Monitor' do
it 'renders the menu in the sidebar' do
visit project_path(project)
- wait_for_requests
- expect(page).to have_selector(
- '.sidebar-sub-level-items a[aria-label="Error Tracking"]',
- text: 'Error Tracking',
- visible: :hidden
- )
+ within_testid('super-sidebar') do
+ expect(page).to have_link('Error Tracking', visible: :hidden)
+ end
end
end
diff --git a/spec/features/projects/settings/registry_settings_cleanup_tags_spec.rb b/spec/features/projects/settings/registry_settings_cleanup_tags_spec.rb
index ee54065fdf8..1b53a6222e6 100644
--- a/spec/features/projects/settings/registry_settings_cleanup_tags_spec.rb
+++ b/spec/features/projects/settings/registry_settings_cleanup_tags_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
RSpec.describe 'Project > Settings > Packages and registries > Container registry tag expiration policy',
feature_category: :groups_and_projects do
- let_it_be(:user) { create(:user, :no_super_sidebar) }
+ let_it_be(:user) { create(:user) }
let_it_be(:project, reload: true) { create(:project, namespace: user.namespace) }
let(:container_registry_enabled) { true }
@@ -24,9 +24,10 @@ RSpec.describe 'Project > Settings > Packages and registries > Container registr
it 'shows active tab on sidebar' do
subject
- expect(find('.sidebar-top-level-items > li.active')).to have_content('Settings')
- expect(find('.sidebar-sub-level-items > li.active:not(.fly-out-top-item)'))
- .to have_content('Packages and registries')
+ within_testid('super-sidebar') do
+ expect(page).to have_selector('button[aria-expanded="true"]', text: 'Settings')
+ expect(page).to have_selector('[aria-current="page"]', text: 'Packages and registries')
+ end
end
it 'shows available section' do
@@ -44,7 +45,7 @@ RSpec.describe 'Project > Settings > Packages and registries > Container registr
wait_for_requests
expect(page).to be_axe_clean.within('[data-testid="container-expiration-policy-project-settings"]')
- .skipping :'link-in-text-block'
+ .skipping :'link-in-text-block', :'heading-order'
end
it 'saves cleanup policy submit the form' do
diff --git a/spec/features/projects/settings/registry_settings_spec.rb b/spec/features/projects/settings/registry_settings_spec.rb
index 7f0367f47f7..d6e08628721 100644
--- a/spec/features/projects/settings/registry_settings_spec.rb
+++ b/spec/features/projects/settings/registry_settings_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
RSpec.describe 'Project > Settings > Packages and registries > Container registry tag expiration policy',
feature_category: :groups_and_projects do
- let_it_be(:user) { create(:user, :no_super_sidebar) }
+ let_it_be(:user) { create(:user) }
let_it_be(:project, reload: true) { create(:project, namespace: user.namespace) }
let(:container_registry_enabled) { true }
@@ -27,15 +27,16 @@ RSpec.describe 'Project > Settings > Packages and registries > Container registr
wait_for_requests
expect(page).to be_axe_clean.within('[data-testid="packages-and-registries-project-settings"]')
- .skipping :'link-in-text-block'
+ .skipping :'link-in-text-block', :'heading-order'
end
it 'shows active tab on sidebar' do
subject
- expect(find('.sidebar-top-level-items > li.active')).to have_content('Settings')
- expect(find('.sidebar-sub-level-items > li.active:not(.fly-out-top-item)'))
- .to have_content('Packages and registries')
+ within_testid('super-sidebar') do
+ expect(page).to have_selector('button[aria-expanded="true"]', text: 'Settings')
+ expect(page).to have_selector('[aria-current="page"]', text: 'Packages and registries')
+ end
end
it 'shows available section' do
diff --git a/spec/features/projects/show/user_sees_collaboration_links_spec.rb b/spec/features/projects/show/user_sees_collaboration_links_spec.rb
index 626d4de7baf..f231b4a591a 100644
--- a/spec/features/projects/show/user_sees_collaboration_links_spec.rb
+++ b/spec/features/projects/show/user_sees_collaboration_links_spec.rb
@@ -6,14 +6,14 @@ RSpec.describe 'Projects > Show > Collaboration links', :js, feature_category: :
using RSpec::Parameterized::TableSyntax
let_it_be(:project) { create(:project, :repository, :public) }
- let_it_be(:user) { create(:user, :no_super_sidebar) }
+ let_it_be(:user) { create(:user) }
before do
sign_in(user)
end
def find_new_menu_toggle
- find('#js-onboarding-new-project-link')
+ find('[data-testid="base-dropdown-toggle"]', text: 'Create new...')
end
context 'with developer user' do
@@ -25,7 +25,7 @@ RSpec.describe 'Projects > Show > Collaboration links', :js, feature_category: :
visit project_path(project)
# The navigation bar
- page.within('.header-new') do
+ within_testid('super-sidebar') do
find_new_menu_toggle.click
aggregate_failures 'dropdown links in the navigation bar' do
@@ -60,7 +60,7 @@ RSpec.describe 'Projects > Show > Collaboration links', :js, feature_category: :
visit project_path(project)
- page.within('.header-new') do
+ within_testid('super-sidebar') do
find_new_menu_toggle.click
aggregate_failures 'dropdown links' do
diff --git a/spec/features/projects/snippets/show_spec.rb b/spec/features/projects/snippets/show_spec.rb
index 12018b4b9d7..e5836739c57 100644
--- a/spec/features/projects/snippets/show_spec.rb
+++ b/spec/features/projects/snippets/show_spec.rb
@@ -3,35 +3,62 @@
require 'spec_helper'
RSpec.describe 'Projects > Snippets > Project snippet', :js, feature_category: :source_code_management do
- let_it_be(:user) { create(:user) }
+ let_it_be(:author) { create(:author) }
let_it_be(:project) do
- create(:project, creator: user).tap do |p|
- p.add_maintainer(user)
+ create(:project, :public, creator: author).tap do |p|
+ p.add_maintainer(author)
end
end
- let_it_be(:snippet) { create(:project_snippet, :repository, project: project, author: user) }
+ let_it_be(:snippet) { create(:project_snippet, :public, :repository, project: project, author: author) }
+ let(:anchor) { nil }
+ let(:file_path) { 'files/ruby/popen.rb' }
+
+ def visit_page
+ visit project_snippet_path(project, snippet, anchor: anchor)
+ end
before do
- sign_in(user)
+ # rubocop: disable RSpec/AnyInstanceOf -- TODO: The usage of let_it_be forces us
+ allow_any_instance_of(Snippet).to receive(:blobs)
+ .and_return([snippet.repository.blob_at('master', file_path)])
+ # rubocop: enable RSpec/AnyInstanceOf
end
- it_behaves_like 'show and render proper snippet blob' do
- let(:anchor) { nil }
+ context 'when signed in' do
+ before do
+ sign_in(user)
+ visit_page
+ end
- subject do
- visit project_snippet_path(project, snippet, anchor: anchor)
+ context 'as project member' do
+ let(:user) { author }
- wait_for_requests
+ it_behaves_like 'show and render proper snippet blob'
+ it_behaves_like 'does show New Snippet button'
end
- end
- # it_behaves_like 'showing user status' do
- # This will be handled in https://gitlab.com/gitlab-org/gitlab/-/issues/262394
+ context 'as external user' do
+ let_it_be(:user) { create(:user, :external) }
+
+ it_behaves_like 'show and render proper snippet blob'
+ it_behaves_like 'does not show New Snippet button'
+ end
- it_behaves_like 'does not show New Snippet button' do
- let(:file_path) { 'files/ruby/popen.rb' }
+ context 'as another user' do
+ let_it_be(:user) { create(:user) }
+
+ it_behaves_like 'show and render proper snippet blob'
+ it_behaves_like 'does not show New Snippet button'
+ end
+ end
+
+ context 'when unauthenticated' do
+ before do
+ visit_page
+ end
- subject { visit project_snippet_path(project, snippet) }
+ it_behaves_like 'show and render proper snippet blob'
+ it_behaves_like 'does not show New Snippet button'
end
end
diff --git a/spec/features/projects/tree/tree_show_spec.rb b/spec/features/projects/tree/tree_show_spec.rb
index 3becc48d450..4a913d82a78 100644
--- a/spec/features/projects/tree/tree_show_spec.rb
+++ b/spec/features/projects/tree/tree_show_spec.rb
@@ -21,6 +21,15 @@ RSpec.describe 'Projects tree', :js, feature_category: :web_ide do
sign_in(user)
end
+ it 'passes axe automated accessibility testing' do
+ visit project_tree_path(project, test_sha)
+ wait_for_requests
+
+ expect(page).to be_axe_clean.within('.project-last-commit')
+ expect(page).to be_axe_clean.within('.nav-block')
+ expect(page).to be_axe_clean.within('.tree-content-holder').skipping :'link-in-text-block'
+ end
+
it 'renders tree table without errors' do
visit project_tree_path(project, test_sha)
wait_for_requests
@@ -111,9 +120,16 @@ RSpec.describe 'Projects tree', :js, feature_category: :web_ide do
end
context 'LFS' do
- it 'renders LFS badge on blob item' do
+ before do
visit project_tree_path(project, File.join('master', 'files/lfs'))
+ wait_for_requests
+ end
+ it 'passes axe automated accessibility testing' do
+ expect(page).to be_axe_clean.within('.tree-content-holder').skipping :'link-in-text-block'
+ end
+
+ it 'renders LFS badge on blob item' do
expect(page).to have_selector('[data-testid="label-lfs"]', text: 'LFS')
end
end
diff --git a/spec/features/projects/user_sees_sidebar_spec.rb b/spec/features/projects/user_sees_sidebar_spec.rb
index 22d00e9a351..61225b45760 100644
--- a/spec/features/projects/user_sees_sidebar_spec.rb
+++ b/spec/features/projects/user_sees_sidebar_spec.rb
@@ -2,8 +2,8 @@
require 'spec_helper'
-RSpec.describe 'Projects > User sees sidebar', feature_category: :groups_and_projects do
- let(:user) { create(:user, :no_super_sidebar) }
+RSpec.describe 'Projects > User sees sidebar', :js, feature_category: :groups_and_projects do
+ let(:user) { create(:user) }
let(:project) { create(:project, :private, public_builds: false, namespace: user.namespace) }
# NOTE: See documented behaviour https://design.gitlab.com/regions/navigation#contextual-navigation
@@ -14,44 +14,25 @@ RSpec.describe 'Projects > User sees sidebar', feature_category: :groups_and_pro
sign_in(user)
end
- shared_examples 'has a expanded nav sidebar' do
- it 'has a expanded desktop nav-sidebar on load' do
- expect(page).to have_content('Collapse sidebar')
- expect(page).not_to have_selector('.sidebar-collapsed-desktop')
- expect(page).not_to have_selector('.sidebar-expanded-mobile')
+ shared_examples 'has an expanded nav sidebar' do
+ it 'has an expanded nav sidebar on load' do
+ expect(page).to have_selector('[data-testid="super-sidebar-collapse-button"]', visible: :visible)
end
- it 'can collapse the nav-sidebar' do
- page.find('.nav-sidebar .js-toggle-sidebar').click
- expect(page).to have_selector('.sidebar-collapsed-desktop')
- expect(page).not_to have_content('Collapse sidebar')
- expect(page).not_to have_selector('.sidebar-expanded-mobile')
+ it 'can collapse the nav sidebar' do
+ find_by_testid('super-sidebar-collapse-button').click
+ expect(page).to have_selector('[data-testid="super-sidebar-collapse-button"]', visible: :hidden)
end
end
shared_examples 'has a collapsed nav sidebar' do
- it 'has a collapsed desktop nav-sidebar on load' do
- expect(page).not_to have_content('Collapse sidebar')
- expect(page).not_to have_selector('.sidebar-expanded-mobile')
+ it 'has a collapsed nav sidebar on load' do
+ expect(page).to have_selector('[data-testid="super-sidebar-collapse-button"]', visible: :hidden)
end
- it 'can expand the nav-sidebar' do
- page.find('.nav-sidebar .js-toggle-sidebar').click
- expect(page).to have_selector('.sidebar-expanded-mobile')
- expect(page).to have_content('Collapse sidebar')
- end
- end
-
- shared_examples 'has a mobile nav-sidebar' do
- it 'has a hidden nav-sidebar on load' do
- expect(page).not_to have_content('.mobile-nav-open')
- expect(page).not_to have_selector('.sidebar-expanded-mobile')
- end
-
- it 'can expand the nav-sidebar' do
- page.find('.toggle-mobile-nav').click
- expect(page).to have_selector('.mobile-nav-open')
- expect(page).to have_selector('.sidebar-expanded-mobile')
+ it 'can expand the nav sidebar' do
+ page.find('.js-super-sidebar-toggle-expand').click
+ expect(page).to have_selector('[data-testid="super-sidebar-collapse-button"]', visible: :visible)
end
end
@@ -59,29 +40,24 @@ RSpec.describe 'Projects > User sees sidebar', feature_category: :groups_and_pro
before do
resize_screen_xs
visit project_path(project)
- expect(page).to have_selector('.nav-sidebar')
- expect(page).to have_selector('.toggle-mobile-nav')
end
- it_behaves_like 'has a mobile nav-sidebar'
+ it_behaves_like 'has a collapsed nav sidebar'
end
context 'with a small size viewport' do
before do
resize_screen_sm
visit project_path(project)
- expect(page).to have_selector('.nav-sidebar')
- expect(page).to have_selector('.toggle-mobile-nav')
end
- it_behaves_like 'has a mobile nav-sidebar'
+ it_behaves_like 'has a collapsed nav sidebar'
end
context 'with medium size viewport' do
before do
resize_window(768, 800)
visit project_path(project)
- expect(page).to have_selector('.nav-sidebar')
end
it_behaves_like 'has a collapsed nav sidebar'
@@ -91,7 +67,6 @@ RSpec.describe 'Projects > User sees sidebar', feature_category: :groups_and_pro
before do
resize_window(1199, 800)
visit project_path(project)
- expect(page).to have_selector('.nav-sidebar')
end
it_behaves_like 'has a collapsed nav sidebar'
@@ -101,10 +76,9 @@ RSpec.describe 'Projects > User sees sidebar', feature_category: :groups_and_pro
before do
resize_window(1200, 800)
visit project_path(project)
- expect(page).to have_selector('.nav-sidebar')
end
- it_behaves_like 'has a expanded nav sidebar'
+ it_behaves_like 'has an expanded nav sidebar'
end
end
@@ -121,8 +95,8 @@ RSpec.describe 'Projects > User sees sidebar', feature_category: :groups_and_pro
it 'does not display a "Snippets" link' do
visit project_path(project)
- within('.nav-sidebar') do
- expect(page).not_to have_content 'Snippets'
+ within_testid('super-sidebar') do
+ expect(page).not_to have_button 'Code'
end
end
end
@@ -182,7 +156,7 @@ RSpec.describe 'Projects > User sees sidebar', feature_category: :groups_and_pro
end
context 'as guest' do
- let(:guest) { create(:user, :no_super_sidebar) }
+ let(:guest) { create(:user) }
let!(:issue) { create(:issue, :opened, project: project, author: guest) }
before do
@@ -194,15 +168,19 @@ RSpec.describe 'Projects > User sees sidebar', feature_category: :groups_and_pro
it 'shows allowed tabs only' do
visit project_path(project)
- within('.nav-sidebar') do
- expect(page).to have_content 'Project'
- expect(page).to have_content 'Issues'
- expect(page).to have_content 'Wiki'
- expect(page).to have_content 'Monitor'
+ within_testid('super-sidebar') do
+ expect(page).to have_button 'Pinned'
+ expect(page).to have_button 'Manage'
+ expect(page).to have_button 'Plan'
+ expect(page).to have_button 'Code'
+ expect(page).to have_button 'Monitor'
+ expect(page).to have_button 'Analyze'
+
+ expect(page).not_to have_button 'Build'
- expect(page).not_to have_content 'Repository'
- expect(page).not_to have_content 'CI/CD'
- expect(page).not_to have_content 'Merge Requests'
+ click_button 'Code'
+ expect(page).not_to have_link 'Repository'
+ expect(page).not_to have_link 'Merge requests'
end
end
@@ -212,8 +190,8 @@ RSpec.describe 'Projects > User sees sidebar', feature_category: :groups_and_pro
visit project_path(project)
- within('.nav-sidebar') do
- expect(page).to have_content 'CI/CD'
+ within_testid('super-sidebar') do
+ expect(page).to have_button 'Build'
end
end
diff --git a/spec/features/projects/user_uses_shortcuts_spec.rb b/spec/features/projects/user_uses_shortcuts_spec.rb
index b7b2093d78a..a000c9e1da8 100644
--- a/spec/features/projects/user_uses_shortcuts_spec.rb
+++ b/spec/features/projects/user_uses_shortcuts_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe 'User uses shortcuts', :js, feature_category: :groups_and_projects do
- let_it_be(:user) { create(:user, :no_super_sidebar) }
+ let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project, :repository, namespace: user.namespace) }
before do
@@ -21,14 +21,14 @@ RSpec.describe 'User uses shortcuts', :js, feature_category: :groups_and_project
find('body').native.send_key('g')
find('body').native.send_key('o')
- expect(page).to have_active_navigation(project.name)
+ expect(page).to have_active_sub_navigation(project.name)
end
it 'redirects to the activity page' do
find('body').native.send_key('g')
find('body').native.send_key('v')
- expect(page).to have_active_navigation('Project')
+ expect(page).to have_active_navigation('Manage')
expect(page).to have_active_sub_navigation('Activity')
end
end
@@ -38,31 +38,39 @@ RSpec.describe 'User uses shortcuts', :js, feature_category: :groups_and_project
find('body').native.send_key('g')
find('body').native.send_key('f')
- expect(page).to have_active_navigation('Repository')
- expect(page).to have_active_sub_navigation('Files')
+ expect(page).to have_active_navigation('Code')
+ expect(page).to have_active_sub_navigation('Repository')
end
- it 'redirects to the repository commits page' do
- find('body').native.send_key('g')
- find('body').native.send_key('c')
+ context 'when hitting the commits controller' do
+ # Hitting the commits controller with the super sidebar enabled seems to trigger more SQL
+ # queries, exceeding the 100 limit. We need to increase the limit a bit for these tests to pass.
+ before do
+ allow(Gitlab::QueryLimiting::Transaction).to receive(:threshold).and_return(110)
+ end
+
+ it 'redirects to the repository commits page' do
+ find('body').native.send_key('g')
+ find('body').native.send_key('c')
- expect(page).to have_active_navigation('Repository')
- expect(page).to have_active_sub_navigation('Commits')
+ expect(page).to have_active_navigation('Code')
+ expect(page).to have_active_sub_navigation('Commits')
+ end
end
it 'redirects to the repository graph page' do
find('body').native.send_key('g')
find('body').native.send_key('n')
- expect(page).to have_active_navigation('Repository')
- expect(page).to have_active_sub_navigation('Graph')
+ expect(page).to have_active_navigation('Code')
+ expect(page).to have_active_sub_navigation('Repository graph')
end
it 'redirects to the repository charts page' do
find('body').native.send_key('g')
find('body').native.send_key('d')
- expect(page).to have_active_navigation(_('Analytics'))
+ expect(page).to have_active_navigation(_('Analyze'))
expect(page).to have_active_sub_navigation(_('Repository'))
end
end
@@ -72,16 +80,16 @@ RSpec.describe 'User uses shortcuts', :js, feature_category: :groups_and_project
find('body').native.send_key('g')
find('body').native.send_key('i')
- expect(page).to have_active_navigation('Issues')
- expect(page).to have_active_sub_navigation('List')
+ expect(page).to have_active_navigation('Pinned')
+ expect(page).to have_active_sub_navigation('Issues')
end
it 'redirects to the issue board page' do
find('body').native.send_key('g')
find('body').native.send_key('b')
- expect(page).to have_active_navigation('Issues')
- expect(page).to have_active_sub_navigation('Board')
+ expect(page).to have_active_navigation('Plan')
+ expect(page).to have_active_sub_navigation('Issue boards')
end
it 'redirects to the new issue page' do
@@ -97,7 +105,8 @@ RSpec.describe 'User uses shortcuts', :js, feature_category: :groups_and_project
find('body').native.send_key('g')
find('body').native.send_key('m')
- expect(page).to have_active_navigation('Merge requests')
+ expect(page).to have_active_navigation('Pinned')
+ expect(page).to have_active_sub_navigation('Merge requests')
end
end
@@ -106,7 +115,7 @@ RSpec.describe 'User uses shortcuts', :js, feature_category: :groups_and_project
find('body').native.send_key('g')
find('body').native.send_key('p')
- expect(page).to have_active_navigation('CI/CD')
+ expect(page).to have_active_navigation('Build')
expect(page).to have_active_sub_navigation('Pipelines')
end
@@ -114,7 +123,7 @@ RSpec.describe 'User uses shortcuts', :js, feature_category: :groups_and_project
find('body').native.send_key('g')
find('body').native.send_key('j')
- expect(page).to have_active_navigation('CI/CD')
+ expect(page).to have_active_navigation('Build')
expect(page).to have_active_sub_navigation('Jobs')
end
end
@@ -124,7 +133,7 @@ RSpec.describe 'User uses shortcuts', :js, feature_category: :groups_and_project
find('body').native.send_key('g')
find('body').native.send_key('e')
- expect(page).to have_active_navigation('Deployments')
+ expect(page).to have_active_navigation('Operate')
expect(page).to have_active_sub_navigation('Environments')
end
end
@@ -134,7 +143,7 @@ RSpec.describe 'User uses shortcuts', :js, feature_category: :groups_and_project
find('body').native.send_key('g')
find('body').native.send_key('k')
- expect(page).to have_active_navigation('Infrastructure')
+ expect(page).to have_active_navigation('Operate')
expect(page).to have_active_sub_navigation('Kubernetes')
end
end
@@ -144,7 +153,8 @@ RSpec.describe 'User uses shortcuts', :js, feature_category: :groups_and_project
find('body').native.send_key('g')
find('body').native.send_key('s')
- expect(page).to have_active_navigation('Snippets')
+ expect(page).to have_active_navigation('Code')
+ expect(page).to have_active_sub_navigation('Snippets')
end
end
@@ -153,7 +163,8 @@ RSpec.describe 'User uses shortcuts', :js, feature_category: :groups_and_project
find('body').native.send_key('g')
find('body').native.send_key('w')
- expect(page).to have_active_navigation('Wiki')
+ expect(page).to have_active_navigation('Plan')
+ expect(page).to have_active_sub_navigation('Wiki')
end
end
end
diff --git a/spec/features/projects/wikis_spec.rb b/spec/features/projects/wikis_spec.rb
index 63714954c0c..5d950da6674 100644
--- a/spec/features/projects/wikis_spec.rb
+++ b/spec/features/projects/wikis_spec.rb
@@ -3,7 +3,7 @@
require "spec_helper"
RSpec.describe 'Project wikis', :js, feature_category: :wiki do
- let_it_be(:user) { create(:user, :no_super_sidebar) }
+ let_it_be(:user) { create(:user) }
let(:wiki) { create(:project_wiki, user: user, project: project) }
let(:project) { create(:project, namespace: user.namespace, creator: user) }
diff --git a/spec/features/projects/work_items/linked_work_items_spec.rb b/spec/features/projects/work_items/linked_work_items_spec.rb
index 66016cf8b7b..49f723c3055 100644
--- a/spec/features/projects/work_items/linked_work_items_spec.rb
+++ b/spec/features/projects/work_items/linked_work_items_spec.rb
@@ -102,8 +102,8 @@ RSpec.describe 'Work item linked items', :js, feature_category: :team_planning d
expect(find('.work-items-list')).to have_content('Task 1')
- find_by_testid('links-menu').click
- click_button 'Remove'
+ find_by_testid('links-child').hover
+ find_by_testid('remove-work-item-link').click
wait_for_all_requests
diff --git a/spec/features/projects/work_items/work_item_children_spec.rb b/spec/features/projects/work_items/work_item_children_spec.rb
index 843afb54dec..752ea282fbf 100644
--- a/spec/features/projects/work_items/work_item_children_spec.rb
+++ b/spec/features/projects/work_items/work_item_children_spec.rb
@@ -87,8 +87,8 @@ RSpec.describe 'Work item children', :js, feature_category: :team_planning do
expect(find('[data-testid="links-child"]')).to have_content('Task 1')
expect(find('[data-testid="children-count"]')).to have_content('1')
- find('[data-testid="links-menu"]').click
- click_button 'Remove'
+ find_by_testid('links-child').hover
+ find_by_testid('remove-work-item-link').click
wait_for_all_requests
diff --git a/spec/features/projects/work_items/work_item_spec.rb b/spec/features/projects/work_items/work_item_spec.rb
index 5210d67b78c..33153d21575 100644
--- a/spec/features/projects/work_items/work_item_spec.rb
+++ b/spec/features/projects/work_items/work_item_spec.rb
@@ -3,11 +3,14 @@
require 'spec_helper'
RSpec.describe 'Work item', :js, feature_category: :team_planning do
- let_it_be_with_reload(:user) { create(:user, :no_super_sidebar) }
- let_it_be_with_reload(:user2) { create(:user, :no_super_sidebar, name: 'John') }
+ include ListboxHelpers
+
+ let_it_be_with_reload(:user) { create(:user) }
+ let_it_be_with_reload(:user2) { create(:user, name: 'John') }
let_it_be(:project) { create(:project, :public) }
let_it_be(:work_item) { create(:work_item, project: project) }
+ let_it_be(:task) { create(:work_item, :task, project: project) }
let_it_be(:emoji_upvote) { create(:award_emoji, :upvote, awardable: work_item, user: user2) }
let_it_be(:milestone) { create(:milestone, project: project) }
let_it_be(:milestones) { create_list(:milestone, 25, project: project) }
@@ -18,9 +21,7 @@ RSpec.describe 'Work item', :js, feature_category: :team_planning do
context 'for signed in user' do
before do
project.add_developer(user)
-
sign_in(user)
-
visit work_items_path
end
@@ -37,7 +38,7 @@ RSpec.describe 'Work item', :js, feature_category: :team_planning do
end
it 'actions dropdown is displayed' do
- expect(page).to have_selector('[data-testid="work-item-actions-dropdown"]')
+ expect(page).to have_button _('More actions')
end
it 'reassigns to another user',
@@ -74,9 +75,7 @@ RSpec.describe 'Work item', :js, feature_category: :team_planning do
context 'for signed in owner' do
before do
project.add_owner(user)
-
sign_in(user)
-
visit work_items_path
end
@@ -86,29 +85,37 @@ RSpec.describe 'Work item', :js, feature_category: :team_planning do
context 'for guest users' do
before do
project.add_guest(user)
-
sign_in(user)
-
visit work_items_path
end
it_behaves_like 'work items comment actions for guest users'
end
+ context 'when item is a task' do
+ before do
+ project.add_developer(user)
+
+ sign_in(user)
+
+ visit project_work_item_path(project, task.iid)
+ end
+
+ it_behaves_like 'work items parent', :issue
+ end
+
context 'for user not signed in' do
before do
visit work_items_path
end
it 'todos action is not displayed' do
- expect(page).not_to have_selector('[data-testid="work-item-todos-action"]')
+ expect(page).not_to have_button s_('WorkItem|Add a to do')
end
it 'award button is disabled and add reaction is not displayed' do
- within('[data-testid="work-item-award-list"]') do
- expect(page).not_to have_selector('[data-testid="emoji-picker"]')
- expect(page).to have_selector('[data-testid="award-button"].disabled')
- end
+ expect(page).not_to have_button _('Add reaction')
+ expect(page).to have_selector('[data-testid="award-button"].disabled')
end
it 'assignees input field is disabled' do
diff --git a/spec/features/projects_spec.rb b/spec/features/projects_spec.rb
index 7ca9395f669..c6966e47f0a 100644
--- a/spec/features/projects_spec.rb
+++ b/spec/features/projects_spec.rb
@@ -7,7 +7,7 @@ RSpec.describe 'Project', feature_category: :groups_and_projects do
include MobileHelpers
describe 'template' do
- let(:user) { create(:user, :no_super_sidebar) }
+ let(:user) { create(:user) }
before do
sign_in user
@@ -78,7 +78,7 @@ RSpec.describe 'Project', feature_category: :groups_and_projects do
end
describe 'shows tip about push to create git command' do
- let(:user) { create(:user, :no_super_sidebar) }
+ let(:user) { create(:user) }
before do
sign_in user
@@ -214,7 +214,7 @@ RSpec.describe 'Project', feature_category: :groups_and_projects do
end
describe 'showing information about source of a project fork', :js do
- let(:user) { create(:user, :no_super_sidebar) }
+ let(:user) { create(:user) }
let(:base_project) { create(:project, :public, :repository) }
let(:forked_project) { fork_project(base_project, user, repository: true) }
@@ -265,7 +265,7 @@ RSpec.describe 'Project', feature_category: :groups_and_projects do
end
describe 'when the project repository is disabled', :js do
- let(:user) { create(:user, :no_super_sidebar) }
+ let(:user) { create(:user) }
let(:project) { create(:project, :repository_disabled, :repository, namespace: user.namespace) }
before do
@@ -282,7 +282,7 @@ RSpec.describe 'Project', feature_category: :groups_and_projects do
end
describe 'removal', :js do
- let(:user) { create(:user, :no_super_sidebar) }
+ let(:user) { create(:user) }
let(:project) { create(:project, namespace: user.namespace) }
before do
@@ -307,7 +307,7 @@ RSpec.describe 'Project', feature_category: :groups_and_projects do
end
describe 'tree view (default view is set to Files)', :js do
- let(:user) { create(:user, :no_super_sidebar, project_view: 'files') }
+ let(:user) { create(:user, project_view: 'files') }
let(:project) { create(:forked_project_with_submodules) }
before do
@@ -379,7 +379,7 @@ RSpec.describe 'Project', feature_category: :groups_and_projects do
end
describe 'activity view' do
- let(:user) { create(:user, :no_super_sidebar, project_view: 'activity') }
+ let(:user) { create(:user, project_view: 'activity') }
let(:project) { create(:project, :repository) }
before do
@@ -410,7 +410,7 @@ RSpec.describe 'Project', feature_category: :groups_and_projects do
end
describe 'edit' do
- let(:user) { create(:user, :no_super_sidebar) }
+ let(:user) { create(:user) }
let(:project) { create(:project, :public) }
let(:path) { edit_project_path(project) }
@@ -425,9 +425,9 @@ RSpec.describe 'Project', feature_category: :groups_and_projects do
describe 'view for a user without an access to a repo' do
let(:project) { create(:project, :repository) }
- let(:user) { create(:user, :no_super_sidebar) }
+ let(:user) { create(:user) }
- it 'does not contain default branch information in its content' do
+ it 'does not contain default branch information in its content', :js do
default_branch = 'merge-commit-analyze-side-branch'
project.add_guest(user)
@@ -436,8 +436,10 @@ RSpec.describe 'Project', feature_category: :groups_and_projects do
sign_in(user)
visit project_path(project)
- lines_with_default_branch = page.html.lines.select { |line| line.include?(default_branch) }
- expect(lines_with_default_branch).to eq([])
+ page.within('#content-body') do
+ lines_with_default_branch = page.html.lines.select { |line| line.include?(default_branch) }
+ expect(lines_with_default_branch).to eq([])
+ end
end
end
diff --git a/spec/features/search/user_searches_for_code_spec.rb b/spec/features/search/user_searches_for_code_spec.rb
index d2847203669..976324a5032 100644
--- a/spec/features/search/user_searches_for_code_spec.rb
+++ b/spec/features/search/user_searches_for_code_spec.rb
@@ -6,7 +6,7 @@ RSpec.describe 'User searches for code', :js, :disable_rate_limiter, feature_cat
using RSpec::Parameterized::TableSyntax
include ListboxHelpers
- let_it_be(:user) { create(:user, :no_super_sidebar) }
+ let_it_be(:user) { create(:user) }
let_it_be_with_reload(:project) { create(:project, :repository, namespace: user.namespace) }
context 'when signed in' do
diff --git a/spec/features/search/user_searches_for_comments_spec.rb b/spec/features/search/user_searches_for_comments_spec.rb
index f47e692c652..f7af1797c71 100644
--- a/spec/features/search/user_searches_for_comments_spec.rb
+++ b/spec/features/search/user_searches_for_comments_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
RSpec.describe 'User searches for comments', :js, :disable_rate_limiter, feature_category: :global_search do
let_it_be(:project) { create(:project, :repository) }
- let_it_be(:user) { create(:user, :no_super_sidebar) }
+ let_it_be(:user) { create(:user) }
before do
project.add_reporter(user)
diff --git a/spec/features/search/user_searches_for_commits_spec.rb b/spec/features/search/user_searches_for_commits_spec.rb
index 140d8763813..724daf9277d 100644
--- a/spec/features/search/user_searches_for_commits_spec.rb
+++ b/spec/features/search/user_searches_for_commits_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe 'User searches for commits', :js, :clean_gitlab_redis_rate_limiting, feature_category: :global_search do
- let_it_be(:user) { create(:user, :no_super_sidebar) }
+ let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project, :repository) }
let(:sha) { '6d394385cf567f80a8fd85055db1ab4c5295806f' }
diff --git a/spec/features/search/user_searches_for_issues_spec.rb b/spec/features/search/user_searches_for_issues_spec.rb
index d816b393cce..9451e337db1 100644
--- a/spec/features/search/user_searches_for_issues_spec.rb
+++ b/spec/features/search/user_searches_for_issues_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe 'User searches for issues', :js, :clean_gitlab_redis_rate_limiting, feature_category: :global_search do
- let_it_be(:user) { create(:user, :no_super_sidebar) }
+ let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project, namespace: user.namespace) }
let!(:issue1) { create(:issue, title: 'issue Foo', project: project, created_at: 1.hour.ago) }
diff --git a/spec/features/search/user_searches_for_merge_requests_spec.rb b/spec/features/search/user_searches_for_merge_requests_spec.rb
index 61af5e86eea..d7b52d9e07a 100644
--- a/spec/features/search/user_searches_for_merge_requests_spec.rb
+++ b/spec/features/search/user_searches_for_merge_requests_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe 'User searches for merge requests', :js, :clean_gitlab_redis_rate_limiting, feature_category: :global_search do
- let_it_be(:user) { create(:user, :no_super_sidebar) }
+ let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project, namespace: user.namespace) }
let_it_be(:merge_request1) { create(:merge_request, title: 'Merge Request Foo', source_project: project, target_project: project, created_at: 1.hour.ago) }
let_it_be(:merge_request2) { create(:merge_request, :simple, title: 'Merge Request Bar', source_project: project, target_project: project) }
diff --git a/spec/features/search/user_searches_for_milestones_spec.rb b/spec/features/search/user_searches_for_milestones_spec.rb
index ad62c8eb3da..7ca7958f61b 100644
--- a/spec/features/search/user_searches_for_milestones_spec.rb
+++ b/spec/features/search/user_searches_for_milestones_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
RSpec.describe 'User searches for milestones', :js, :clean_gitlab_redis_rate_limiting,
feature_category: :global_search do
- let_it_be(:user) { create(:user, :no_super_sidebar) }
+ let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project, namespace: user.namespace) }
let_it_be(:milestone1) { create(:milestone, title: 'Foo', project: project) }
let_it_be(:milestone2) { create(:milestone, title: 'Bar', project: project) }
diff --git a/spec/features/search/user_searches_for_projects_spec.rb b/spec/features/search/user_searches_for_projects_spec.rb
index 51e5ad85e2b..48a94161927 100644
--- a/spec/features/search/user_searches_for_projects_spec.rb
+++ b/spec/features/search/user_searches_for_projects_spec.rb
@@ -8,7 +8,7 @@ RSpec.describe 'User searches for projects', :js, :disable_rate_limiter, feature
context 'when signed out' do
context 'when block_anonymous_global_searches is disabled' do
before do
- stub_feature_flags(block_anonymous_global_searches: false, super_sidebar_logged_out: false)
+ stub_feature_flags(block_anonymous_global_searches: false)
end
include_examples 'top right search form'
diff --git a/spec/features/search/user_searches_for_users_spec.rb b/spec/features/search/user_searches_for_users_spec.rb
index b52f6aeba68..e0a07c5103d 100644
--- a/spec/features/search/user_searches_for_users_spec.rb
+++ b/spec/features/search/user_searches_for_users_spec.rb
@@ -3,9 +3,9 @@
require 'spec_helper'
RSpec.describe 'User searches for users', :js, :clean_gitlab_redis_rate_limiting, feature_category: :global_search do
- let_it_be(:user1) { create(:user, :no_super_sidebar, username: 'gob_bluth', name: 'Gob Bluth') }
- let_it_be(:user2) { create(:user, :no_super_sidebar, username: 'michael_bluth', name: 'Michael Bluth') }
- let_it_be(:user3) { create(:user, :no_super_sidebar, username: 'gob_2018', name: 'George Oscar Bluth') }
+ let_it_be(:user1) { create(:user, username: 'gob_bluth', name: 'Gob Bluth') }
+ let_it_be(:user2) { create(:user, username: 'michael_bluth', name: 'Michael Bluth') }
+ let_it_be(:user3) { create(:user, username: 'gob_2018', name: 'George Oscar Bluth') }
before do
sign_in(user1)
diff --git a/spec/features/search/user_searches_for_wiki_pages_spec.rb b/spec/features/search/user_searches_for_wiki_pages_spec.rb
index a5b63243d0b..65f262075f9 100644
--- a/spec/features/search/user_searches_for_wiki_pages_spec.rb
+++ b/spec/features/search/user_searches_for_wiki_pages_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
RSpec.describe 'User searches for wiki pages', :js, :clean_gitlab_redis_rate_limiting,
feature_category: :global_search do
- let_it_be(:user) { create(:user, :no_super_sidebar) }
+ let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project, :repository, :wiki_repo, namespace: user.namespace) }
let_it_be(:wiki_page) do
create(:wiki_page, wiki: project.wiki, title: 'directory/title', content: 'Some Wiki content')
diff --git a/spec/features/search/user_uses_header_search_field_spec.rb b/spec/features/search/user_uses_header_search_field_spec.rb
index 3f2a71b63dc..1ab47f6fd59 100644
--- a/spec/features/search/user_uses_header_search_field_spec.rb
+++ b/spec/features/search/user_uses_header_search_field_spec.rb
@@ -6,8 +6,8 @@ RSpec.describe 'User uses header search field', :js, :disable_rate_limiter, feat
include FilteredSearchHelpers
let_it_be(:project) { create(:project, :repository) }
- let_it_be(:reporter) { create(:user, :no_super_sidebar) }
- let_it_be(:developer) { create(:user, :no_super_sidebar) }
+ let_it_be(:reporter) { create(:user) }
+ let_it_be(:developer) { create(:user) }
let(:user) { reporter }
@@ -31,12 +31,6 @@ RSpec.describe 'User uses header search field', :js, :disable_rate_limiter, feat
submit_search('gitlab')
end
- it 'renders page title' do
- page.within('.page-title') do
- expect(page).to have_content('Search')
- end
- end
-
it 'renders breadcrumbs' do
page.within('.breadcrumbs') do
expect(page).to have_content('Search')
@@ -46,31 +40,34 @@ RSpec.describe 'User uses header search field', :js, :disable_rate_limiter, feat
context 'when using the keyboard shortcut' do
before do
- find('#search')
find('body').native.send_keys('s')
- wait_for_all_requests
end
- it 'shows the category search dropdown', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/250285' do
- expect(page).to have_selector('.dropdown-header', text: /#{scope_name}/i)
+ it 'shows the search modal' do
+ expect(page).to have_selector(search_modal_results, visible: :visible)
end
end
- context 'when clicking the search field' do
+ context 'when clicking the search button' do
before do
- page.find('#search').click
+ within_testid('super-sidebar') do
+ click_button "Search or go to…"
+ end
wait_for_all_requests
end
- it 'shows category search dropdown', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/250285' do
- expect(page).to have_selector('.dropdown-header', text: /#{scope_name}/i)
+ it 'shows search scope badge' do
+ fill_in 'search', with: 'text'
+ within('#super-sidebar-search-modal') do
+ expect(page).to have_selector('.search-scope-help', text: scope_name)
+ end
end
context 'when clicking issues', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/332317' do
let!(:issue) { create(:issue, project: project, author: user, assignees: [user]) }
it 'shows assigned issues' do
- find('[data-testid="header-search-dropdown-menu"]').click_link('Issues assigned to me')
+ find(search_modal_results).click_link('Issues assigned to me')
expect(page).to have_selector('.issues-list .issue')
expect_tokens([assignee_token(user.name)])
@@ -78,7 +75,7 @@ RSpec.describe 'User uses header search field', :js, :disable_rate_limiter, feat
end
it 'shows created issues' do
- find('[data-testid="header-search-dropdown-menu"]').click_link("Issues I've created")
+ find(search_modal_results).click_link("Issues I've created")
expect(page).to have_selector('.issues-list .issue')
expect_tokens([author_token(user.name)])
@@ -90,7 +87,7 @@ RSpec.describe 'User uses header search field', :js, :disable_rate_limiter, feat
let!(:merge_request) { create(:merge_request, source_project: project, author: user, assignees: [user]) }
it 'shows assigned merge requests' do
- find('[data-testid="header-search-dropdown-menu"]').click_link('Merge requests assigned to me')
+ find(search_modal_results).click_link('Merge requests assigned to me')
expect(page).to have_selector('.mr-list .merge-request')
expect_tokens([assignee_token(user.name)])
@@ -98,7 +95,7 @@ RSpec.describe 'User uses header search field', :js, :disable_rate_limiter, feat
end
it 'shows created merge requests' do
- find('[data-testid="header-search-dropdown-menu"]').click_link("Merge requests I've created")
+ find(search_modal_results).click_link("Merge requests I've created")
expect(page).to have_selector('.mr-list .merge-request')
expect_tokens([author_token(user.name)])
@@ -119,7 +116,7 @@ RSpec.describe 'User uses header search field', :js, :disable_rate_limiter, feat
context 'when user is in a global scope' do
include_examples 'search field examples' do
let(:url) { root_path }
- let(:scope_name) { 'All GitLab' }
+ let(:scope_name) { 'in all GitLab' }
end
it 'displays search options', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/251076' do
@@ -136,11 +133,13 @@ RSpec.describe 'User uses header search field', :js, :disable_rate_limiter, feat
end
it 'displays result counts for all categories' do
- expect(page).to have_content('Projects 1')
- expect(page).to have_content('Issues 1')
- expect(page).to have_content('Merge requests 0')
- expect(page).to have_content('Milestones 0')
- expect(page).to have_content('Users 0')
+ within_testid('super-sidebar') do
+ expect(page).to have_link('Projects 1')
+ expect(page).to have_link('Issues 1')
+ expect(page).to have_link('Merge requests 0')
+ expect(page).to have_link('Milestones 0')
+ expect(page).to have_link('Users 0')
+ end
end
end
end
@@ -162,9 +161,8 @@ RSpec.describe 'User uses header search field', :js, :disable_rate_limiter, feat
it 'displays search options' do
fill_in_search('test')
- expect(page).to have_selector(scoped_search_link('test', search_code: true))
expect(page).to have_selector(scoped_search_link('test', group_id: group.id, search_code: true))
- expect(page).to have_selector(scoped_search_link('test', project_id: project.id, group_id: group.id, search_code: true))
+ expect(page).to have_selector(scoped_search_link('test', search_code: true))
end
end
@@ -176,26 +174,25 @@ RSpec.describe 'User uses header search field', :js, :disable_rate_limiter, feat
it 'displays search options' do
fill_in_search('test')
- sleep 0.5
- expect(page).to have_selector(scoped_search_link('test', search_code: true, repository_ref: 'master'))
+
expect(page).not_to have_selector(scoped_search_link('test', search_code: true, group_id: project.namespace_id, repository_ref: 'master'))
- expect(page).to have_selector(scoped_search_link('test', search_code: true, project_id: project.id, repository_ref: 'master'))
+ expect(page).to have_selector(scoped_search_link('test', search_code: true, repository_ref: 'master'))
end
it 'displays a link to project merge requests' do
fill_in_search('Merge')
- within(dashboard_search_options_popup_menu) do
- expect(page).to have_text('Merge requests')
+ within(search_modal_results) do
+ expect(page).to have_link('Merge requests')
end
end
it 'does not display a link to project feature flags' do
fill_in_search('Feature')
- within(dashboard_search_options_popup_menu) do
- expect(page).to have_text('Feature in all GitLab')
- expect(page).to have_no_text('Feature Flags')
+ within(search_modal_results) do
+ expect(page).to have_link('in all GitLab Feature')
+ expect(page).not_to have_link('Feature Flags')
end
end
@@ -205,8 +202,8 @@ RSpec.describe 'User uses header search field', :js, :disable_rate_limiter, feat
it 'displays a link to project feature flags' do
fill_in_search('Feature')
- within(dashboard_search_options_popup_menu) do
- expect(page).to have_text('Feature Flags')
+ within(search_modal_results) do
+ expect(page).to have_link('Feature Flags')
end
end
end
@@ -228,8 +225,8 @@ RSpec.describe 'User uses header search field', :js, :disable_rate_limiter, feat
it 'displays search options' do
fill_in_search('test')
+
expect(page).to have_selector(scoped_search_link('test'))
- expect(page).to have_selector(scoped_search_link('test', group_id: group.id))
expect(page).not_to have_selector(scoped_search_link('test', project_id: project.id))
end
end
@@ -253,7 +250,6 @@ RSpec.describe 'User uses header search field', :js, :disable_rate_limiter, feat
fill_in_search('test')
expect(page).to have_selector(scoped_search_link('test'))
- expect(page).to have_selector(scoped_search_link('test', group_id: subgroup.id))
expect(page).not_to have_selector(scoped_search_link('test', project_id: project.id))
end
end
@@ -268,10 +264,10 @@ RSpec.describe 'User uses header search field', :js, :disable_rate_limiter, feat
href.concat("&search_code=true") if search_code
href.concat("&repository_ref=#{repository_ref}") if repository_ref
- "[data-testid='header-search-dropdown-menu'] a[href='#{href}']"
+ ".global-search-results a[href='#{href}']"
end
- def dashboard_search_options_popup_menu
- "[data-testid='header-search-dropdown-menu'] .header-search-dropdown-content"
+ def search_modal_results
+ ".global-search-results"
end
end
diff --git a/spec/features/snippets/search_snippets_spec.rb b/spec/features/snippets/search_snippets_spec.rb
index 7a07299a14f..cf6f9825932 100644
--- a/spec/features/snippets/search_snippets_spec.rb
+++ b/spec/features/snippets/search_snippets_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
RSpec.describe 'Search Snippets', :js, feature_category: :global_search do
it 'user searches for snippets by title' do
- user = create(:user, :no_super_sidebar)
+ user = create(:user)
public_snippet = create(:personal_snippet, :public, title: 'Beginning and Middle')
private_snippet = create(:personal_snippet, :private, title: 'Middle and End', author: user)
diff --git a/spec/features/snippets/show_spec.rb b/spec/features/snippets/show_spec.rb
index bbb120edb80..03f46ea0122 100644
--- a/spec/features/snippets/show_spec.rb
+++ b/spec/features/snippets/show_spec.rb
@@ -3,49 +3,63 @@
require 'spec_helper'
RSpec.describe 'Snippet', :js, feature_category: :source_code_management do
- let_it_be(:user) { create(:user, :no_super_sidebar) }
- let_it_be(:snippet) { create(:personal_snippet, :public, :repository, author: user) }
+ let_it_be(:owner) { create(:user) }
+ let_it_be(:snippet) { create(:personal_snippet, :public, :repository, author: owner) }
+ let(:anchor) { nil }
+ let(:file_path) { 'files/ruby/popen.rb' }
before do
- stub_feature_flags(super_sidebar_logged_out: false)
+ # rubocop: disable RSpec/AnyInstanceOf -- TODO: The usage of let_it_be forces us
+ allow_any_instance_of(Snippet).to receive(:blobs)
+ .and_return([snippet.repository.blob_at('master', file_path)])
+ # rubocop: enable RSpec/AnyInstanceOf
end
- it_behaves_like 'show and render proper snippet blob' do
- let(:anchor) { nil }
-
- subject do
- visit snippet_path(snippet, anchor: anchor)
+ def visit_page
+ visit snippet_path(snippet, anchor: anchor)
+ end
- wait_for_requests
+ context 'when signed in' do
+ before do
+ sign_in(user)
+ visit_page
end
- end
- # it_behaves_like 'showing user status' do
- # This will be handled in https://gitlab.com/gitlab-org/gitlab/-/issues/262394
+ context 'as the snippet owner' do
+ let(:user) { owner }
- it_behaves_like 'does not show New Snippet button' do
- let(:file_path) { 'files/ruby/popen.rb' }
+ it_behaves_like 'show and render proper snippet blob'
+ it_behaves_like 'does show New Snippet button'
+ it_behaves_like 'a "Your work" page with sidebar and breadcrumbs', :dashboard_snippets_path, :snippets
+ end
- subject { visit snippet_path(snippet) }
- end
+ context 'as external user' do
+ let_it_be(:user) { create(:user, :external) }
- it_behaves_like 'a "Your work" page with sidebar and breadcrumbs', :dashboard_snippets_path, :snippets
+ it_behaves_like 'show and render proper snippet blob'
+ it_behaves_like 'does not show New Snippet button'
+ it_behaves_like 'a "Your work" page with sidebar and breadcrumbs', :dashboard_snippets_path, :snippets
+ end
- context 'when unauthenticated' do
- it 'shows the "Explore" sidebar' do
- visit snippet_path(snippet)
+ context 'as another user' do
+ let_it_be(:user) { create(:user) }
- expect(page).to have_css('aside.nav-sidebar[aria-label="Explore"]')
+ it_behaves_like 'show and render proper snippet blob'
+ it_behaves_like 'does show New Snippet button'
+ it_behaves_like 'a "Your work" page with sidebar and breadcrumbs', :dashboard_snippets_path, :snippets
end
end
- context 'when authenticated as a different user' do
- let_it_be(:different_user) { create(:user, :no_super_sidebar) }
-
+ context 'when unauthenticated' do
before do
- sign_in(different_user)
+ visit_page
end
- it_behaves_like 'a "Your work" page with sidebar and breadcrumbs', :dashboard_snippets_path, :snippets
+ it_behaves_like 'show and render proper snippet blob'
+ it_behaves_like 'does not show New Snippet button'
+
+ it 'shows the "Explore" sidebar' do
+ expect(page).to have_css('#super-sidebar-context-header', text: 'Explore')
+ end
end
end
diff --git a/spec/features/snippets/user_creates_snippet_spec.rb b/spec/features/snippets/user_creates_snippet_spec.rb
index 341cc150a64..f1f804786a3 100644
--- a/spec/features/snippets/user_creates_snippet_spec.rb
+++ b/spec/features/snippets/user_creates_snippet_spec.rb
@@ -6,7 +6,7 @@ RSpec.describe 'User creates snippet', :js, feature_category: :source_code_manag
include DropzoneHelper
include Features::SnippetSpecHelpers
- let_it_be(:user) { create(:user, :no_super_sidebar) }
+ let_it_be(:user) { create(:user) }
let(:title) { 'My Snippet Title' }
let(:file_content) { 'Hello World!' }
diff --git a/spec/features/task_lists_spec.rb b/spec/features/task_lists_spec.rb
index 24d63cadf00..c1be2b8e3c7 100644
--- a/spec/features/task_lists_spec.rb
+++ b/spec/features/task_lists_spec.rb
@@ -6,8 +6,8 @@ RSpec.describe 'Task Lists', :js, feature_category: :team_planning do
include Warden::Test::Helpers
let_it_be(:project) { create(:project, :public, :repository) }
- let_it_be(:user) { create(:user, :no_super_sidebar) }
- let_it_be(:user2) { create(:user, :no_super_sidebar) }
+ let_it_be(:user) { create(:user) }
+ let_it_be(:user2) { create(:user) }
let(:markdown) do
<<-MARKDOWN.strip_heredoc
@@ -44,7 +44,7 @@ RSpec.describe 'Task Lists', :js, feature_category: :team_planning do
end
before do
- login_as(user)
+ sign_in(user)
end
def visit_issue(project, issue)
diff --git a/spec/features/unsubscribe_links_spec.rb b/spec/features/unsubscribe_links_spec.rb
index b78efa65888..77ef3df97f6 100644
--- a/spec/features/unsubscribe_links_spec.rb
+++ b/spec/features/unsubscribe_links_spec.rb
@@ -6,8 +6,8 @@ RSpec.describe 'Unsubscribe links', :sidekiq_inline, feature_category: :shared d
include Warden::Test::Helpers
let_it_be(:project) { create(:project, :public) }
- let_it_be(:author) { create(:user, :no_super_sidebar).tap { |u| project.add_reporter(u) } }
- let_it_be(:recipient) { create(:user, :no_super_sidebar) }
+ let_it_be(:author) { create(:user).tap { |u| project.add_reporter(u) } }
+ let_it_be(:recipient) { create(:user) }
let(:params) { { title: 'A bug!', description: 'Fix it!', assignee_ids: [recipient.id] } }
let(:issue) { Issues::CreateService.new(container: project, current_user: author, params: params).execute[:issue] }
@@ -22,10 +22,6 @@ RSpec.describe 'Unsubscribe links', :sidekiq_inline, feature_category: :shared d
end
context 'when logged out' do
- before do
- stub_feature_flags(super_sidebar_logged_out: false)
- end
-
context 'when visiting the link from the body' do
it 'shows the unsubscribe confirmation page and redirects to root path when confirming' do
visit body_link
diff --git a/spec/features/uploads/user_uploads_avatar_to_profile_spec.rb b/spec/features/uploads/user_uploads_avatar_to_profile_spec.rb
index 5de544e866e..83eb7cb989e 100644
--- a/spec/features/uploads/user_uploads_avatar_to_profile_spec.rb
+++ b/spec/features/uploads/user_uploads_avatar_to_profile_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe 'User uploads avatar to profile', feature_category: :user_profile do
- let!(:user) { create(:user, :no_super_sidebar) }
+ let!(:user) { create(:user) }
let(:avatar_file_path) { Rails.root.join('spec', 'fixtures', 'dk.png') }
shared_examples 'upload avatar' do
@@ -19,8 +19,7 @@ RSpec.describe 'User uploads avatar to profile', feature_category: :user_profile
wait_for_all_requests
data_uri = find('.avatar-image .gl-avatar')['src']
- expect(page.find('.header-user-avatar')['src']).to eq data_uri
- expect(page.find('[data-testid="sidebar-user-avatar"]')['src']).to eq data_uri
+ within_testid('user-dropdown') { expect(find('.gl-avatar')['src']).to eq data_uri }
visit profile_path
diff --git a/spec/features/usage_stats_consent_spec.rb b/spec/features/usage_stats_consent_spec.rb
index 92f7a944007..ebf1cd9e143 100644
--- a/spec/features/usage_stats_consent_spec.rb
+++ b/spec/features/usage_stats_consent_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
RSpec.describe 'Usage stats consent', feature_category: :service_ping do
context 'when signed in' do
- let(:user) { create(:admin, :no_super_sidebar, created_at: 8.days.ago) }
+ let(:user) { create(:admin, created_at: 8.days.ago) }
let(:message) { 'To help improve GitLab, we would like to periodically collect usage information.' }
before do
@@ -22,24 +22,29 @@ RSpec.describe 'Usage stats consent', feature_category: :service_ping do
gitlab_enable_admin_mode_sign_in(user)
end
- it 'hides the banner permanently when sets usage stats' do
- visit root_dashboard_path
+ shared_examples 'dismissible banner' do |button_text|
+ it 'hides the banner permanently when sets usage stats', :js do
+ visit root_dashboard_path
- expect(page).to have_content(message)
+ expect(page).to have_content(message)
- click_link 'Send service data'
+ click_link button_text
- expect(page).not_to have_content(message)
- expect(page).to have_content('Application settings saved successfully')
+ expect(page).not_to have_content(message)
+ expect(page).to have_content('Application settings saved successfully')
- gitlab_sign_out
- gitlab_sign_in(user)
- visit root_dashboard_path
+ gitlab_sign_out
+ gitlab_sign_in(user)
+ visit root_dashboard_path
- expect(page).not_to have_content(message)
+ expect(page).not_to have_content(message)
+ end
end
- it 'shows banner on next session if user did not set usage stats' do
+ it_behaves_like 'dismissible banner', _('Send service data')
+ it_behaves_like 'dismissible banner', _("Don't send service data")
+
+ it 'shows banner on next session if user did not set usage stats', :js do
visit root_dashboard_path
expect(page).to have_content(message)
diff --git a/spec/features/user_can_display_performance_bar_spec.rb b/spec/features/user_can_display_performance_bar_spec.rb
index caf13c4111b..a22418760aa 100644
--- a/spec/features/user_can_display_performance_bar_spec.rb
+++ b/spec/features/user_can_display_performance_bar_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'User can display performance bar', :js, feature_category: :application_performance do
+RSpec.describe 'User can display performance bar', :js, feature_category: :cloud_connector do
shared_examples 'performance bar cannot be displayed' do
it 'does not show the performance bar by default' do
expect(page).not_to have_css('#js-peek')
diff --git a/spec/features/user_sees_active_nav_items_spec.rb b/spec/features/user_sees_active_nav_items_spec.rb
new file mode 100644
index 00000000000..966b8491374
--- /dev/null
+++ b/spec/features/user_sees_active_nav_items_spec.rb
@@ -0,0 +1,63 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'User sees correct active nav items in the super sidebar', :js, feature_category: :value_stream_management do
+ let_it_be(:current_user) { create(:user) }
+
+ before do
+ sign_in(current_user)
+ end
+
+ describe 'profile pages' do
+ context 'when visiting profile page' do
+ before do
+ visit profile_path
+ end
+
+ it 'renders the side navigation with the correct submenu set as active' do
+ expect(page).to have_active_sub_navigation('Profile')
+ end
+ end
+
+ context 'when visiting preferences page' do
+ before do
+ visit profile_preferences_path
+ end
+
+ it 'renders the side navigation with the correct submenu set as active' do
+ expect(page).to have_active_sub_navigation('Preferences')
+ end
+ end
+
+ context 'when visiting authentication logs' do
+ before do
+ visit audit_log_profile_path
+ end
+
+ it 'renders the side navigation with the correct submenu set as active' do
+ expect(page).to have_active_sub_navigation('Authentication Log')
+ end
+ end
+
+ context 'when visiting SSH keys page' do
+ before do
+ visit profile_keys_path
+ end
+
+ it 'renders the side navigation with the correct submenu set as active' do
+ expect(page).to have_active_sub_navigation('SSH Keys')
+ end
+ end
+
+ context 'when visiting account page' do
+ before do
+ visit profile_account_path
+ end
+
+ it 'renders the side navigation with the correct submenu set as active' do
+ expect(page).to have_active_sub_navigation('Account')
+ end
+ end
+ end
+end
diff --git a/spec/features/user_sees_revert_modal_spec.rb b/spec/features/user_sees_revert_modal_spec.rb
index fdeee6a2808..ebb84a0d87f 100644
--- a/spec/features/user_sees_revert_modal_spec.rb
+++ b/spec/features/user_sees_revert_modal_spec.rb
@@ -12,6 +12,8 @@ RSpec.describe 'Merge request > User sees revert modal', :js, :sidekiq_might_not
it 'shows the revert modal' do
click_button('Revert')
+ wait_for_requests
+
page.within('[data-testid="modal-commit"]') do
expect(page).to have_content 'Revert this merge request'
end
@@ -19,7 +21,6 @@ RSpec.describe 'Merge request > User sees revert modal', :js, :sidekiq_might_not
end
before do
- stub_feature_flags(unbatch_graphql_queries: false)
sign_in(user)
visit(project_merge_request_path(project, merge_request))
@@ -27,6 +28,10 @@ RSpec.describe 'Merge request > User sees revert modal', :js, :sidekiq_might_not
click_button 'Merge'
end
+ wait_for_all_requests
+
+ page.refresh
+
wait_for_requests
end
diff --git a/spec/features/users/active_sessions_spec.rb b/spec/features/users/active_sessions_spec.rb
index 663d2283dbd..8509a8d7356 100644
--- a/spec/features/users/active_sessions_spec.rb
+++ b/spec/features/users/active_sessions_spec.rb
@@ -3,10 +3,10 @@
require 'spec_helper'
RSpec.describe 'Active user sessions', :clean_gitlab_redis_sessions, feature_category: :system_access do
- it 'successful login adds a new active user login' do
- user = create(:user, :no_super_sidebar)
+ it 'successful login adds a new active user login', :js do
+ user = create(:user)
- now = Time.zone.parse('2018-03-12 09:06')
+ now = Time.zone.now.change(usec: 0)
travel_to(now) do
gitlab_sign_in(user)
expect(page).to have_current_path root_path, ignore_query: true
@@ -24,14 +24,14 @@ RSpec.describe 'Active user sessions', :clean_gitlab_redis_sessions, feature_cat
sessions = ActiveSession.list(user)
expect(sessions.first).to have_attributes(
- created_at: Time.zone.parse('2018-03-12 09:06'),
- updated_at: Time.zone.parse('2018-03-12 09:07')
+ created_at: now,
+ updated_at: now + 1.minute
)
end
end
it 'successful login cleans up obsolete entries' do
- user = create(:user, :no_super_sidebar)
+ user = create(:user)
Gitlab::Redis::Sessions.with do |redis|
redis.sadd?("session:lookup:user:gitlab:#{user.id}", '59822c7d9fcdfa03725eff41782ad97d')
@@ -45,7 +45,7 @@ RSpec.describe 'Active user sessions', :clean_gitlab_redis_sessions, feature_cat
end
it 'sessionless login does not clean up obsolete entries' do
- user = create(:user, :no_super_sidebar)
+ user = create(:user)
personal_access_token = create(:personal_access_token, user: user)
Gitlab::Redis::Sessions.with do |redis|
@@ -60,8 +60,8 @@ RSpec.describe 'Active user sessions', :clean_gitlab_redis_sessions, feature_cat
end
end
- it 'logout deletes the active user login' do
- user = create(:user, :no_super_sidebar)
+ it 'logout deletes the active user login', :js do
+ user = create(:user)
gitlab_sign_in(user)
expect(page).to have_current_path root_path, ignore_query: true
diff --git a/spec/features/users/anonymous_sessions_spec.rb b/spec/features/users/anonymous_sessions_spec.rb
index 368f272ba23..81b18b7ca02 100644
--- a/spec/features/users/anonymous_sessions_spec.rb
+++ b/spec/features/users/anonymous_sessions_spec.rb
@@ -5,6 +5,10 @@ require 'spec_helper'
RSpec.describe 'Session TTLs', :clean_gitlab_redis_shared_state, feature_category: :system_access do
include SessionHelpers
+ before do
+ expire_session
+ end
+
it 'creates a session with a short TTL when login fails' do
visit new_user_session_path
# The session key only gets created after a post
@@ -18,10 +22,10 @@ RSpec.describe 'Session TTLs', :clean_gitlab_redis_shared_state, feature_categor
end
it 'increases the TTL when the login succeeds' do
- user = create(:user, :no_super_sidebar)
+ user = create(:user)
gitlab_sign_in(user)
- expect(page).to have_content(user.name)
+ expect(find('.js-super-sidebar')['data-sidebar']).to include(user.name)
expect_single_session_with_authenticated_ttl
end
diff --git a/spec/features/users/email_verification_on_login_spec.rb b/spec/features/users/email_verification_on_login_spec.rb
index d83040efd72..ad62af6ec69 100644
--- a/spec/features/users/email_verification_on_login_spec.rb
+++ b/spec/features/users/email_verification_on_login_spec.rb
@@ -5,8 +5,8 @@ require 'spec_helper'
RSpec.describe 'Email Verification On Login', :clean_gitlab_redis_rate_limiting, :js, feature_category: :system_access do
include EmailHelpers
- let_it_be_with_reload(:user) { create(:user, :no_super_sidebar) }
- let_it_be(:another_user) { create(:user, :no_super_sidebar) }
+ let_it_be_with_reload(:user) { create(:user) }
+ let_it_be(:another_user) { create(:user) }
let_it_be(:new_email) { build_stubbed(:user).email }
let(:require_email_verification_enabled) { user }
@@ -220,7 +220,7 @@ RSpec.describe 'Email Verification On Login', :clean_gitlab_redis_rate_limiting,
shared_examples 'no email verification required when 2fa enabled or ff disabled' do
context 'when 2FA is enabled' do
- let_it_be(:user) { create(:user, :no_super_sidebar, :two_factor) }
+ let_it_be(:user) { create(:user, :two_factor) }
it_behaves_like 'no email verification required', two_factor_auth: true
end
diff --git a/spec/features/users/login_spec.rb b/spec/features/users/login_spec.rb
index 87afcbd416b..0f086af227c 100644
--- a/spec/features/users/login_spec.rb
+++ b/spec/features/users/login_spec.rb
@@ -16,7 +16,7 @@ RSpec.describe 'Login', :clean_gitlab_redis_sessions, feature_category: :system_
expect(authentication_metrics)
.to increment(:user_authenticated_counter)
- user = create(:user, :no_super_sidebar)
+ user = create(:user)
expect(user.reset_password_token).to be_nil
@@ -43,7 +43,7 @@ RSpec.describe 'Login', :clean_gitlab_redis_sessions, feature_category: :system_
# This behavior is dependent on there only being one user
User.delete_all
- user = create(:admin, :no_super_sidebar, password_automatically_set: true)
+ user = create(:admin, password_automatically_set: true)
visit root_path
expect(page).to have_current_path edit_user_password_path, ignore_query: true
@@ -77,7 +77,7 @@ RSpec.describe 'Login', :clean_gitlab_redis_sessions, feature_category: :system_
.and increment(:user_unauthenticated_counter)
.and increment(:user_session_destroyed_counter).twice
- user = create(:user, :no_super_sidebar, :blocked)
+ user = create(:user, :blocked)
gitlab_sign_in(user)
@@ -90,14 +90,14 @@ RSpec.describe 'Login', :clean_gitlab_redis_sessions, feature_category: :system_
.and increment(:user_unauthenticated_counter)
.and increment(:user_session_destroyed_counter).twice
- user = create(:user, :no_super_sidebar, :blocked)
+ user = create(:user, :blocked)
expect { gitlab_sign_in(user) }.not_to change { user.reload.sign_in_count }
end
end
describe 'with an unconfirmed email address' do
- let!(:user) { create(:user, :no_super_sidebar, confirmed_at: nil) }
+ let!(:user) { create(:user, confirmed_at: nil) }
let(:grace_period) { 2.days }
let(:alert_title) { 'Please confirm your email address' }
let(:alert_message) { "To continue, you need to select the link in the confirmation email we sent to verify your email address. If you didn't get our email, select Resend confirmation email" }
@@ -141,7 +141,7 @@ RSpec.describe 'Login', :clean_gitlab_redis_sessions, feature_category: :system_
end
context 'when resending the confirmation email' do
- let_it_be(:user) { create(:user, :no_super_sidebar) }
+ let_it_be(:user) { create(:user) }
it 'redirects to the "almost there" page' do
visit new_user_confirmation_path
@@ -154,7 +154,7 @@ RSpec.describe 'Login', :clean_gitlab_redis_sessions, feature_category: :system_
end
describe 'with a disallowed password' do
- let(:user) { create(:user, :no_super_sidebar, :disallowed_password) }
+ let(:user) { create(:user, :disallowed_password) }
before do
expect(authentication_metrics)
@@ -295,7 +295,7 @@ RSpec.describe 'Login', :clean_gitlab_redis_sessions, feature_category: :system_
# Freeze time to prevent failures when time between code being entered and
# validated greater than otp_allowed_drift
context 'with valid username/password', :freeze_time do
- let(:user) { create(:user, :no_super_sidebar, :two_factor) }
+ let(:user) { create(:user, :two_factor) }
before do
gitlab_sign_in(user, remember: true)
@@ -372,13 +372,13 @@ RSpec.describe 'Login', :clean_gitlab_redis_sessions, feature_category: :system_
end
context 'when user with TOTP enabled' do
- let(:user) { create(:user, :no_super_sidebar, :two_factor) }
+ let(:user) { create(:user, :two_factor) }
include_examples 'can login with recovery codes'
end
context 'when user with only Webauthn enabled' do
- let(:user) { create(:user, :no_super_sidebar, :two_factor_via_webauthn, registrations_count: 1) }
+ let(:user) { create(:user, :two_factor_via_webauthn, registrations_count: 1) }
include_examples 'can login with recovery codes', only_two_factor_webauthn_enabled: true
end
@@ -494,7 +494,7 @@ RSpec.describe 'Login', :clean_gitlab_redis_sessions, feature_category: :system_
end
context 'with correct username and password' do
- let(:user) { create(:user, :no_super_sidebar) }
+ let(:user) { create(:user) }
it 'allows basic login' do
expect(authentication_metrics)
@@ -584,7 +584,7 @@ RSpec.describe 'Login', :clean_gitlab_redis_sessions, feature_category: :system_
end
context 'with correct username and invalid password' do
- let(:user) { create(:user, :no_super_sidebar) }
+ let(:user) { create(:user) }
it 'blocks invalid login' do
expect(authentication_metrics)
@@ -601,7 +601,7 @@ RSpec.describe 'Login', :clean_gitlab_redis_sessions, feature_category: :system_
end
describe 'with required two-factor authentication enabled' do
- let(:user) { create(:user, :no_super_sidebar) }
+ let(:user) { create(:user) }
# TODO: otp_grace_period_started_at
@@ -639,7 +639,7 @@ RSpec.describe 'Login', :clean_gitlab_redis_sessions, feature_category: :system_
end
context 'after the grace period' do
- let(:user) { create(:user, :no_super_sidebar, otp_grace_period_started_at: 9999.hours.ago) }
+ let(:user) { create(:user, otp_grace_period_started_at: 9999.hours.ago) }
it 'redirects to two-factor configuration page' do
expect(authentication_metrics)
@@ -728,7 +728,7 @@ RSpec.describe 'Login', :clean_gitlab_redis_sessions, feature_category: :system_
end
context 'after the grace period' do
- let(:user) { create(:user, :no_super_sidebar, otp_grace_period_started_at: 9999.hours.ago) }
+ let(:user) { create(:user, otp_grace_period_started_at: 9999.hours.ago) }
it 'redirects to two-factor configuration page' do
expect(authentication_metrics)
@@ -919,7 +919,7 @@ RSpec.describe 'Login', :clean_gitlab_redis_sessions, feature_category: :system_
end
context 'when terms are enforced', :js do
- let(:user) { create(:user, :no_super_sidebar) }
+ let(:user) { create(:user) }
before do
enforce_terms
@@ -1090,7 +1090,7 @@ RSpec.describe 'Login', :clean_gitlab_redis_sessions, feature_category: :system_
end
context 'when sending confirmation email and not yet confirmed' do
- let!(:user) { create(:user, :no_super_sidebar, confirmed_at: nil) }
+ let!(:user) { create(:user, confirmed_at: nil) }
let(:grace_period) { 2.days }
let(:alert_title) { 'Please confirm your email address' }
let(:alert_message) { "To continue, you need to select the link in the confirmation email we sent to verify your email address. If you didn't get our email, select Resend confirmation email" }
diff --git a/spec/features/users/logout_spec.rb b/spec/features/users/logout_spec.rb
index d0e5be8dca3..c9839247e7d 100644
--- a/spec/features/users/logout_spec.rb
+++ b/spec/features/users/logout_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe 'Logout/Sign out', :js, feature_category: :system_access do
- let(:user) { create(:user, :no_super_sidebar) }
+ let(:user) { create(:user) }
before do
sign_in(user)
diff --git a/spec/features/users/overview_spec.rb b/spec/features/users/overview_spec.rb
index d1ff60b6069..1da61ecb868 100644
--- a/spec/features/users/overview_spec.rb
+++ b/spec/features/users/overview_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe 'Overview tab on a user profile', :js, feature_category: :user_profile do
- let(:user) { create(:user, :no_super_sidebar) }
+ let(:user) { create(:user) }
let(:contributed_project) { create(:project, :public, :repository) }
def push_code_contribution
@@ -27,8 +27,7 @@ RSpec.describe 'Overview tab on a user profile', :js, feature_category: :user_pr
shared_context 'visit overview tab' do
before do
visit user.username
- page.find('.js-overview-tab a').click
- wait_for_requests
+ click_nav user.name
end
end
@@ -61,15 +60,15 @@ RSpec.describe 'Overview tab on a user profile', :js, feature_category: :user_pr
end
end
- describe 'user has 11 activities' do
+ describe 'user has 15 activities' do
before do
- 11.times { push_code_contribution }
+ 16.times { push_code_contribution }
end
include_context 'visit overview tab'
- it 'displays 10 entries in the list of activities' do
- expect(find('#js-overview')).to have_selector('.event-item', count: 10)
+ it 'displays 15 entries in the list of activities' do
+ expect(find('#js-overview')).to have_selector('.event-item', count: 15)
end
it 'shows a link to the activity list' do
@@ -158,8 +157,7 @@ RSpec.describe 'Overview tab on a user profile', :js, feature_category: :user_pr
describe 'user has no followers' do
before do
visit user.username
- page.find('.js-followers-tab a').click
- wait_for_requests
+ click_nav 'Followers'
end
it 'shows an empty followers list with an info message' do
@@ -177,8 +175,7 @@ RSpec.describe 'Overview tab on a user profile', :js, feature_category: :user_pr
before do
follower.follow(user)
visit user.username
- page.find('.js-followers-tab a').click
- wait_for_requests
+ click_nav 'Followers'
end
it 'shows followers' do
@@ -199,8 +196,7 @@ RSpec.describe 'Overview tab on a user profile', :js, feature_category: :user_pr
end
visit user.username
- page.find('.js-followers-tab a').click
- wait_for_requests
+ click_nav 'Followers'
end
it 'shows paginated followers' do
page.within('#followers') do
@@ -221,8 +217,7 @@ RSpec.describe 'Overview tab on a user profile', :js, feature_category: :user_pr
describe 'user is not following others' do
before do
visit user.username
- page.find('.js-following-tab a').click
- wait_for_requests
+ click_nav 'Following'
end
it 'shows an empty following list with an info message' do
@@ -240,8 +235,7 @@ RSpec.describe 'Overview tab on a user profile', :js, feature_category: :user_pr
before do
user.follow(followee)
visit user.username
- page.find('.js-following-tab a').click
- wait_for_requests
+ click_nav 'Following'
end
it 'shows following user' do
@@ -262,8 +256,7 @@ RSpec.describe 'Overview tab on a user profile', :js, feature_category: :user_pr
end
visit user.username
- page.find('.js-following-tab a').click
- wait_for_requests
+ click_nav 'Following'
end
it 'shows paginated following' do
page.within('#following') do
@@ -286,8 +279,7 @@ RSpec.describe 'Overview tab on a user profile', :js, feature_category: :user_pr
shared_context "visit bot's overview tab" do
before do
visit bot_user.username
- page.find('.js-overview-tab a').click
- wait_for_requests
+ click_nav bot_user.name
end
end
@@ -327,4 +319,13 @@ RSpec.describe 'Overview tab on a user profile', :js, feature_category: :user_pr
end
end
end
+
+ private
+
+ def click_nav(title)
+ within_testid('super-sidebar') do
+ click_link title
+ end
+ wait_for_requests
+ end
end
diff --git a/spec/features/users/rss_spec.rb b/spec/features/users/rss_spec.rb
index 99451ac472d..730c31df899 100644
--- a/spec/features/users/rss_spec.rb
+++ b/spec/features/users/rss_spec.rb
@@ -3,58 +3,35 @@
require 'spec_helper'
RSpec.describe 'User RSS', feature_category: :user_profile do
- let(:user) { create(:user, :no_super_sidebar) }
- let(:path) { user_path(create(:user, :no_super_sidebar)) }
+ let(:user) { create(:user) }
+ let(:path) { user_path(create(:user)) }
- describe 'with "user_profile_overflow_menu_vue" feature flag off' do
+ context 'when signed in' do
before do
- stub_feature_flags(user_profile_overflow_menu_vue: false)
+ sign_in(user)
+ visit path
end
- context 'when signed in' do
- before do
- sign_in(user)
- visit path
+ it 'shows the RSS link with overflow menu', :js do
+ page.within('.user-cover-block') do
+ find_by_testid('base-dropdown-toggle').click
end
- it_behaves_like "it has an RSS button with current_user's feed token"
- end
-
- context 'when signed out' do
- before do
- stub_feature_flags(super_sidebar_logged_out: false)
- visit path
- end
-
- it_behaves_like "it has an RSS button without a feed token"
+ expect(page).to have_link 'Subscribe', href: /feed_token=glft-.*-#{user.id}/
end
end
- describe 'with "user_profile_overflow_menu_vue" feature flag on', :js do
- context 'when signed in' do
- before do
- sign_in(user)
- visit path
- end
-
- it 'shows the RSS link with overflow menu' do
- find('[data-testid="base-dropdown-toggle"').click
-
- expect(page).to have_link 'Subscribe', href: /feed_token=glft-.*-#{user.id}/
- end
+ context 'when signed out' do
+ before do
+ visit path
end
- context 'when signed out' do
- before do
- stub_feature_flags(super_sidebar_logged_out: false)
- visit path
+ it 'has an RSS without a feed token', :js do
+ page.within('.user-cover-block') do
+ find_by_testid('base-dropdown-toggle').click
end
- it 'has an RSS without a feed token' do
- find('[data-testid="base-dropdown-toggle"').click
-
- expect(page).not_to have_link 'Subscribe', href: /feed_token=glft-.*-#{user.id}/
- end
+ expect(page).not_to have_link 'Subscribe', href: /feed_token=glft-.*-#{user.id}/
end
end
end
diff --git a/spec/features/users/show_spec.rb b/spec/features/users/show_spec.rb
index 522eb12f507..2821e8286a4 100644
--- a/spec/features/users/show_spec.rb
+++ b/spec/features/users/show_spec.rb
@@ -7,38 +7,16 @@ RSpec.describe 'User page', feature_category: :user_profile do
let_it_be(:user) { create(:user, bio: '<b>Lorem</b> <i>ipsum</i> dolor sit <a href="https://example.com">amet</a>') }
- before do
- stub_feature_flags(super_sidebar_logged_out: false)
- end
-
subject(:visit_profile) { visit(user_path(user)) }
- context 'with "user_profile_overflow_menu_vue" feature flag enabled', :js do
- it 'does not show the user id in the profile info' do
- subject
-
- expect(page).not_to have_content("User ID: #{user.id}")
- end
-
- it 'shows copy user id action in the dropdown' do
- subject
-
- find('[data-testid="base-dropdown-toggle"').click
-
- expect(page).to have_content("Copy user ID: #{user.id}")
- end
- end
+ it 'shows copy user id action in the dropdown', :js do
+ subject
- context 'with "user_profile_overflow_menu_vue" feature flag disabled', :js do
- before do
- stub_feature_flags(user_profile_overflow_menu_vue: false)
+ page.within('.user-cover-block') do
+ find_by_testid('base-dropdown-toggle').click
end
- it 'shows user id' do
- subject
-
- expect(page).to have_content("User ID: #{user.id}")
- end
+ expect(page).to have_content("Copy user ID: #{user.id}")
end
it 'shows name on breadcrumbs' do
@@ -193,33 +171,37 @@ RSpec.describe 'User page', feature_category: :user_profile do
expect(page).not_to have_button(text: 'Follow', class: 'gl-button')
end
- shared_examples 'follower tabs with count badges' do
- it 'shows 0 followers and 0 following' do
+ shared_examples 'follower links with count badges' do
+ it 'shows no count if no followers / following' do
subject
- expect(page).to have_content('Followers 0')
- expect(page).to have_content('Following 0')
+ within_testid('super-sidebar') do
+ expect(page).to have_link(text: 'Followers')
+ expect(page).to have_link(text: 'Following')
+ end
end
- it 'shows 1 followers and 1 following' do
+ it 'shows count if followers / following' do
follower.follow(user)
user.follow(followee)
subject
- expect(page).to have_content('Followers 1')
- expect(page).to have_content('Following 1')
+ within_testid('super-sidebar') do
+ expect(page).to have_link(text: 'Followers 1')
+ expect(page).to have_link(text: 'Following 1')
+ end
end
end
- it_behaves_like 'follower tabs with count badges'
+ it_behaves_like 'follower links with count badges'
context 'with profile_tabs_vue feature flag disabled' do
before_all do
stub_feature_flags(profile_tabs_vue: false)
end
- it_behaves_like 'follower tabs with count badges'
+ it_behaves_like 'follower links with count badges'
end
it 'does show button to follow' do
diff --git a/spec/features/users/signup_spec.rb b/spec/features/users/signup_spec.rb
index 968308938d1..d873c4846fd 100644
--- a/spec/features/users/signup_spec.rb
+++ b/spec/features/users/signup_spec.rb
@@ -54,7 +54,7 @@ RSpec.shared_examples 'Signup name validation' do |field, max_length, label|
end
end
-RSpec.describe 'Signup', :js, feature_category: :user_profile do
+RSpec.describe 'Signup', :js, feature_category: :user_management do
include TermsHelper
let(:new_user) { build_stubbed(:user) }
diff --git a/spec/features/users/snippets_spec.rb b/spec/features/users/snippets_spec.rb
index 98ac9fa5f92..3a56b371a8c 100644
--- a/spec/features/users/snippets_spec.rb
+++ b/spec/features/users/snippets_spec.rb
@@ -4,10 +4,10 @@ require 'spec_helper'
RSpec.describe 'Snippets tab on a user profile', :js, feature_category: :source_code_management do
context 'when the user has snippets' do
- let(:user) { create(:user, :no_super_sidebar) }
+ let(:user) { create(:user) }
before do
- stub_feature_flags(profile_tabs_vue: false, super_sidebar_logged_out: false)
+ stub_feature_flags(profile_tabs_vue: false)
end
context 'pagination' do
@@ -16,7 +16,7 @@ RSpec.describe 'Snippets tab on a user profile', :js, feature_category: :source_
before do
allow(Snippet).to receive(:default_per_page).and_return(1)
visit user_path(user)
- page.within('.user-profile-nav') { click_link 'Snippets' }
+ within_testid('super-sidebar') { click_link 'Snippets' }
wait_for_requests
end
@@ -30,9 +30,9 @@ RSpec.describe 'Snippets tab on a user profile', :js, feature_category: :source_
let!(:other_snippet) { create(:snippet, :public) }
it 'contains only internal and public snippets of a user when a user is logged in' do
- sign_in(create(:user, :no_super_sidebar))
+ sign_in(create(:user))
visit user_path(user)
- page.within('.user-profile-nav') { click_link 'Snippets' }
+ within_testid('super-sidebar') { click_link 'Snippets' }
wait_for_requests
expect(page).to have_selector('.snippet-row', count: 2)
@@ -43,7 +43,7 @@ RSpec.describe 'Snippets tab on a user profile', :js, feature_category: :source_
it 'contains only public snippets of a user when a user is not logged in' do
visit user_path(user)
- page.within('.user-profile-nav') { click_link 'Snippets' }
+ within_testid('super-sidebar') { click_link 'Snippets' }
wait_for_requests
expect(page).to have_selector('.snippet-row', count: 1)
diff --git a/spec/features/users/terms_spec.rb b/spec/features/users/terms_spec.rb
index e51ed3a0e80..28191587572 100644
--- a/spec/features/users/terms_spec.rb
+++ b/spec/features/users/terms_spec.rb
@@ -27,7 +27,7 @@ RSpec.describe 'Users > Terms', :js, feature_category: :user_profile do
end
context 'when user is a project bot' do
- let(:project_bot) { create(:user, :no_super_sidebar, :project_bot) }
+ let(:project_bot) { create(:user, :project_bot) }
before do
enforce_terms
@@ -42,7 +42,7 @@ RSpec.describe 'Users > Terms', :js, feature_category: :user_profile do
end
context 'when user is a service account' do
- let(:service_account) { create(:user, :no_super_sidebar, :service_account) }
+ let(:service_account) { create(:user, :service_account) }
before do
enforce_terms
@@ -57,7 +57,7 @@ RSpec.describe 'Users > Terms', :js, feature_category: :user_profile do
end
context 'when signed in' do
- let(:user) { create(:user, :no_super_sidebar) }
+ let(:user) { create(:user) }
before do
sign_in(user)
@@ -115,7 +115,7 @@ RSpec.describe 'Users > Terms', :js, feature_category: :user_profile do
# Application settings are cached for a minute
travel_to 2.minutes.from_now do
- within('.nav-sidebar') do
+ within('.contextual-nav') do
click_link 'Issues'
end
diff --git a/spec/features/users/user_browses_projects_on_user_page_spec.rb b/spec/features/users/user_browses_projects_on_user_page_spec.rb
index 5e047192e7b..039b1bbe5b1 100644
--- a/spec/features/users/user_browses_projects_on_user_page_spec.rb
+++ b/spec/features/users/user_browses_projects_on_user_page_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe 'Users > User browses projects on user page', :js, feature_category: :groups_and_projects do
- let!(:user) { create(:user, :no_super_sidebar) }
+ let!(:user) { create(:user) }
let!(:private_project) do
create :project, :private, name: 'private', namespace: user.namespace do |project|
project.add_maintainer(user)
@@ -23,13 +23,13 @@ RSpec.describe 'Users > User browses projects on user page', :js, feature_catego
end
def click_nav_link(name)
- page.within '.nav-links' do
+ within_testid('super-sidebar') do
click_link name
end
end
before do
- stub_feature_flags(profile_tabs_vue: false, super_sidebar_logged_out: false)
+ stub_feature_flags(profile_tabs_vue: false)
end
it 'hides loading spinner after load', :js do
@@ -87,7 +87,7 @@ RSpec.describe 'Users > User browses projects on user page', :js, feature_catego
end
context 'when signed in as another user' do
- let(:another_user) { create(:user, :no_super_sidebar) }
+ let(:another_user) { create(:user) }
before do
sign_in(another_user)
diff --git a/spec/features/webauthn_spec.rb b/spec/features/webauthn_spec.rb
index 52e2b375187..72463a0b9ab 100644
--- a/spec/features/webauthn_spec.rb
+++ b/spec/features/webauthn_spec.rb
@@ -15,7 +15,7 @@ RSpec.describe 'Using WebAuthn Devices for Authentication', :js, feature_categor
# TODO: it_behaves_like 'hardware device for 2fa', 'WebAuthn'
describe 'registration' do
- let(:user) { create(:user, :no_super_sidebar) }
+ let(:user) { create(:user) }
before do
gitlab_sign_in(user)
@@ -58,7 +58,7 @@ RSpec.describe 'Using WebAuthn Devices for Authentication', :js, feature_categor
gitlab_sign_out
# Second user
- user = create(:user, :no_super_sidebar)
+ user = create(:user)
gitlab_sign_in(user)
visit profile_account_path
enable_two_factor_authentication
@@ -126,7 +126,7 @@ RSpec.describe 'Using WebAuthn Devices for Authentication', :js, feature_categor
it_behaves_like 'hardware device for 2fa', 'WebAuthn'
describe 'registration' do
- let(:user) { create(:user, :no_super_sidebar) }
+ let(:user) { create(:user) }
before do
gitlab_sign_in(user)
@@ -161,7 +161,7 @@ RSpec.describe 'Using WebAuthn Devices for Authentication', :js, feature_categor
gitlab_sign_out
# Second user
- user = create(:user, :no_super_sidebar)
+ user = create(:user)
gitlab_sign_in(user)
user.update_attribute(:otp_required_for_login, true)
visit profile_account_path
@@ -227,7 +227,7 @@ RSpec.describe 'Using WebAuthn Devices for Authentication', :js, feature_categor
describe 'authentication' do
let(:otp_required_for_login) { true }
- let(:user) { create(:user, :no_super_sidebar, webauthn_xid: WebAuthn.generate_user_id, otp_required_for_login: otp_required_for_login) }
+ let(:user) { create(:user, webauthn_xid: WebAuthn.generate_user_id, otp_required_for_login: otp_required_for_login) }
let!(:webauthn_device) do
add_webauthn_device(app_id, user)
end
@@ -256,7 +256,7 @@ RSpec.describe 'Using WebAuthn Devices for Authentication', :js, feature_categor
describe 'when a given WebAuthn device has already been registered by another user' do
describe 'but not the current user' do
- let(:other_user) { create(:user, :no_super_sidebar, webauthn_xid: WebAuthn.generate_user_id, otp_required_for_login: otp_required_for_login) }
+ let(:other_user) { create(:user, webauthn_xid: WebAuthn.generate_user_id, otp_required_for_login: otp_required_for_login) }
it 'does not allow logging in with that particular device' do
# Register other user with a different WebAuthn device
@@ -277,7 +277,7 @@ RSpec.describe 'Using WebAuthn Devices for Authentication', :js, feature_categor
it "allows logging in with that particular device" do
pending("support for passing credential options in FakeClient")
# Register current user with the same WebAuthn device
- current_user = create(:user, :no_super_sidebar)
+ current_user = create(:user)
gitlab_sign_in(current_user)
visit profile_account_path
manage_two_factor_authentication
diff --git a/spec/features/whats_new_spec.rb b/spec/features/whats_new_spec.rb
index c8bcf5f6ef0..887994106b6 100644
--- a/spec/features/whats_new_spec.rb
+++ b/spec/features/whats_new_spec.rb
@@ -2,36 +2,28 @@
require "spec_helper"
-RSpec.describe "renders a `whats new` dropdown item", feature_category: :onboarding do
- let_it_be(:user) { create(:user, :no_super_sidebar) }
+RSpec.describe "renders a `whats new` dropdown item", :js, feature_category: :onboarding do
+ let_it_be(:user) { create(:user) }
context 'when not logged in' do
- before do
- stub_feature_flags(super_sidebar_logged_out: false)
- end
-
it 'and on SaaS it renders', :saas do
visit user_path(user)
- page.within '.header-help' do
- find('.header-help-dropdown-toggle').click
+ within_testid('super-sidebar') { click_on 'Help' }
- expect(page).to have_button(text: "What's new")
- end
+ expect(page).to have_button(text: "What's new")
end
it "doesn't render what's new" do
visit user_path(user)
- page.within '.header-help' do
- find('.header-help-dropdown-toggle').click
+ within_testid('super-sidebar') { click_on 'Help' }
- expect(page).not_to have_button(text: "What's new")
- end
+ expect(page).not_to have_button(text: "What's new")
end
end
- context 'when logged in', :js do
+ context 'when logged in' do
before do
sign_in(user)
end
@@ -40,7 +32,7 @@ RSpec.describe "renders a `whats new` dropdown item", feature_category: :onboard
Gitlab::CurrentSettings.update!(whats_new_variant: ApplicationSetting.whats_new_variants[:all_tiers])
visit root_dashboard_path
- find('.header-help-dropdown-toggle').click
+ within_testid('super-sidebar') { click_on 'Help' }
expect(page).to have_button(text: "What's new")
end
@@ -49,7 +41,7 @@ RSpec.describe "renders a `whats new` dropdown item", feature_category: :onboard
Gitlab::CurrentSettings.update!(whats_new_variant: ApplicationSetting.whats_new_variants[:disabled])
visit root_dashboard_path
- find('.header-help-dropdown-toggle').click
+ within_testid('super-sidebar') { click_on 'Help' }
expect(page).not_to have_button(text: "What's new")
end
@@ -57,24 +49,24 @@ RSpec.describe "renders a `whats new` dropdown item", feature_category: :onboard
it 'shows notification dot and count and removes it once viewed' do
visit root_dashboard_path
- page.within '.header-help' do
- expect(page).to have_selector('.notification-dot', visible: true)
+ within_testid('super-sidebar') do
+ click_on 'Help'
+ button = find_button(text: "What's new")
- find('.header-help-dropdown-toggle').click
+ has_testid?('notification-dot', visible: true)
+ expect(button).to have_selector('.badge-pill')
- expect(page).to have_button(text: "What's new")
- expect(page).to have_selector('.js-whats-new-notification-count')
-
- find('button', text: "What's new").click
+ button.click
end
find('.whats-new-drawer .gl-drawer-close-button').click
- find('.header-help-dropdown-toggle').click
- page.within '.header-help' do
- expect(page).not_to have_selector('.notification-dot', visible: true)
- expect(page).to have_button(text: "What's new")
- expect(page).not_to have_selector('.js-whats-new-notification-count')
+ within_testid('super-sidebar') do
+ click_on 'Help'
+ button = find_button(text: "What's new")
+
+ has_testid?('notification-dot', visible: false)
+ expect(button).not_to have_selector('.badge-pill')
end
end
end
diff --git a/spec/finders/ci/catalog/resources/versions_finder_spec.rb b/spec/finders/ci/catalog/resources/versions_finder_spec.rb
new file mode 100644
index 00000000000..b2418aa45dd
--- /dev/null
+++ b/spec/finders/ci/catalog/resources/versions_finder_spec.rb
@@ -0,0 +1,106 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Ci::Catalog::Resources::VersionsFinder, feature_category: :pipeline_composition do
+ include_context 'when there are catalog resources with versions'
+
+ let(:sort) { nil }
+ let(:latest) { nil }
+ let(:params) { { sort: sort, latest: latest }.compact }
+
+ subject(:execute) { described_class.new([resource1, resource2], current_user, params).execute }
+
+ it 'avoids N+1 queries when authorizing multiple catalog resources', :request_store do
+ control_count = ActiveRecord::QueryRecorder.new { execute }
+
+ # A new user is required to avoid a false positive from cached user authorization queries
+ new_user = create(:user)
+
+ expect do
+ described_class.new([resource1, resource2, resource3], new_user, params).execute
+ end.not_to exceed_query_limit(control_count)
+ end
+
+ context 'when the user is not authorized for any catalog resource' do
+ it 'returns empty response' do
+ is_expected.to be_empty
+ end
+ end
+
+ describe 'versions' do
+ before_all do
+ resource1.project.add_guest(current_user)
+ end
+
+ it 'returns the versions of the authorized catalog resource' do
+ expect(execute).to match_array([v1_0, v1_1])
+ end
+
+ context 'with sort parameter' do
+ it 'returns versions ordered by released_at descending by default' do
+ expect(execute).to eq([v1_1, v1_0])
+ end
+
+ context 'when sort is released_at_asc' do
+ let(:sort) { 'released_at_asc' }
+
+ it 'returns versions ordered by released_at ascending' do
+ expect(execute).to eq([v1_0, v1_1])
+ end
+ end
+
+ context 'when sort is created_asc' do
+ let(:sort) { 'created_asc' }
+
+ it 'returns versions ordered by created_at ascending' do
+ expect(execute).to eq([v1_1, v1_0])
+ end
+ end
+
+ context 'when sort is created_desc' do
+ let(:sort) { 'created_desc' }
+
+ it 'returns versions ordered by created_at descending' do
+ expect(execute).to eq([v1_0, v1_1])
+ end
+ end
+ end
+
+ it 'preloads associations' do
+ expect(Ci::Catalog::Resources::Version).to receive(:preloaded).once.and_call_original
+
+ execute
+ end
+ end
+
+ describe 'latest versions' do
+ before_all do
+ resource1.project.add_guest(current_user)
+ resource2.project.add_guest(current_user)
+ end
+
+ let(:latest) { true }
+
+ it 'returns the latest version for each authorized catalog resource' do
+ expect(execute).to match_array([v1_1, v2_1])
+ end
+
+ context 'when one catalog resource does not have versions' do
+ it 'returns the latest version of only the catalog resource with versions' do
+ resource1.versions.delete_all(:delete_all)
+
+ is_expected.to match_array([v2_1])
+ end
+ end
+
+ context 'when no catalog resource has versions' do
+ it 'returns empty response' do
+ resource1.versions.delete_all(:delete_all)
+ resource2.versions.delete_all(:delete_all)
+
+ is_expected.to be_empty
+ end
+ end
+ end
+end
diff --git a/spec/finders/ci/runners_finder_spec.rb b/spec/finders/ci/runners_finder_spec.rb
index 06cca035c6f..7f680f50297 100644
--- a/spec/finders/ci/runners_finder_spec.rb
+++ b/spec/finders/ci/runners_finder_spec.rb
@@ -148,6 +148,22 @@ RSpec.describe Ci::RunnersFinder, feature_category: :runner_fleet do
described_class.new(current_user: admin, params: { tag_name: %w[tag1 tag2] }).execute
end
end
+
+ context 'by creator' do
+ it 'calls the corresponding scope on Ci::Runner' do
+ expect(Ci::Runner).to receive(:with_creator_id).with('1').and_call_original
+
+ described_class.new(current_user: admin, params: { creator_id: '1' }).execute
+ end
+ end
+
+ context 'by version' do
+ it 'calls the corresponding scope on Ci::Runner' do
+ expect(Ci::Runner).to receive(:with_version_prefix).with('15.').and_call_original
+
+ described_class.new(current_user: admin, params: { version_prefix: '15.' }).execute
+ end
+ end
end
context 'sorting' do
@@ -291,6 +307,9 @@ RSpec.describe Ci::RunnersFinder, feature_category: :runner_fleet do
let_it_be(:runner_project_5) { create(:ci_runner, :project, contacted_at: 3.minutes.ago, tag_list: %w[runner_tag], projects: [project_4]) }
let_it_be(:runner_project_6) { create(:ci_runner, :project, contacted_at: 2.minutes.ago, projects: [project_5]) }
let_it_be(:runner_project_7) { create(:ci_runner, :project, contacted_at: 1.minute.ago, projects: [project_6]) }
+ let_it_be(:runner_manager_1) { create(:ci_runner_machine, runner: runner_sub_group_1, version: '15.11.0') }
+ let_it_be(:runner_manager_2) { create(:ci_runner_machine, runner: runner_sub_group_2, version: '15.11.1') }
+ let_it_be(:runner_manager_3) { create(:ci_runner_machine, runner: runner_sub_group_3, version: '15.10.1') }
let(:target_group) { nil }
let(:membership) { nil }
@@ -431,6 +450,32 @@ RSpec.describe Ci::RunnersFinder, feature_category: :runner_fleet do
runner_project_3, runner_project_2, runner_project_1])
end
end
+
+ context 'by version prefix' do
+ context 'search by major version' do
+ let(:extra_params) { { version_prefix: '15.' } }
+
+ it 'returns correct runner' do
+ is_expected.to contain_exactly(runner_sub_group_1, runner_sub_group_2, runner_sub_group_3)
+ end
+ end
+
+ context 'search by minor version' do
+ let(:extra_params) { { version_prefix: '15.11.' } }
+
+ it 'returns correct runner' do
+ is_expected.to contain_exactly(runner_sub_group_1, runner_sub_group_2)
+ end
+ end
+
+ context 'search by patch version' do
+ let(:extra_params) { { version_prefix: '15.11.1' } }
+
+ it 'returns correct runner' do
+ is_expected.to contain_exactly(runner_sub_group_2)
+ end
+ end
+ end
end
end
end
@@ -560,6 +605,7 @@ RSpec.describe Ci::RunnersFinder, feature_category: :runner_fleet do
let_it_be(:runner_project_active) { create(:ci_runner, :project, contacted_at: 5.minutes.ago, active: true, projects: [project]) }
let_it_be(:runner_project_inactive) { create(:ci_runner, :project, contacted_at: 5.minutes.ago, active: false, projects: [project]) }
let_it_be(:runner_other_project_inactive) { create(:ci_runner, :project, contacted_at: 5.minutes.ago, active: false, projects: [other_project]) }
+ let_it_be(:runner_manager) { create(:ci_runner_machine, runner: runner_instance_inactive, version: '15.10.0') }
context 'by search term' do
let_it_be(:runner_project_1) { create(:ci_runner, :project, contacted_at: 5.minutes.ago, description: 'runner_project_search', projects: [project]) }
@@ -608,6 +654,24 @@ RSpec.describe Ci::RunnersFinder, feature_category: :runner_fleet do
expect(subject).to match_array([runner_project_active, runner_project_inactive])
end
end
+
+ context 'by creator' do
+ let_it_be(:runner_creator_1) { create(:ci_runner, creator_id: '1') }
+
+ let(:extra_params) { { creator_id: '1' } }
+
+ it 'returns correct runners' do
+ is_expected.to contain_exactly(runner_creator_1)
+ end
+ end
+
+ context 'by version prefix' do
+ let(:extra_params) { { version_prefix: '15.' } }
+
+ it 'returns correct runners' do
+ is_expected.to contain_exactly(runner_instance_inactive)
+ end
+ end
end
end
diff --git a/spec/finders/data_transfer/mocked_transfer_finder_spec.rb b/spec/finders/data_transfer/mocked_transfer_finder_spec.rb
deleted file mode 100644
index f60bc98f587..00000000000
--- a/spec/finders/data_transfer/mocked_transfer_finder_spec.rb
+++ /dev/null
@@ -1,22 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe DataTransfer::MockedTransferFinder, feature_category: :source_code_management do
- describe '#execute' do
- subject(:execute) { described_class.new.execute }
-
- it 'returns mock data' do
- expect(execute.first).to include(
- date: '2023-01-01',
- repository_egress: be_a(Integer),
- artifacts_egress: be_a(Integer),
- packages_egress: be_a(Integer),
- registry_egress: be_a(Integer),
- total_egress: be_a(Integer)
- )
-
- expect(execute.size).to eq(12)
- end
- end
-end
diff --git a/spec/finders/milestones_finder_spec.rb b/spec/finders/milestones_finder_spec.rb
index c4c62e21ad9..118679a4911 100644
--- a/spec/finders/milestones_finder_spec.rb
+++ b/spec/finders/milestones_finder_spec.rb
@@ -22,7 +22,7 @@ RSpec.describe MilestonesFinder do
end
it 'returns milestones for groups' do
- result = described_class.new(group_ids: group.id, state: 'all').execute
+ result = described_class.new(group_ids: group.id, state: 'all').execute
expect(result).to contain_exactly(milestone_5, milestone_1, milestone_2)
end
diff --git a/spec/finders/organizations/user_organizations_finder_spec.rb b/spec/finders/organizations/user_organizations_finder_spec.rb
new file mode 100644
index 00000000000..71c9b861831
--- /dev/null
+++ b/spec/finders/organizations/user_organizations_finder_spec.rb
@@ -0,0 +1,50 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Organizations::UserOrganizationsFinder, '#execute', feature_category: :cell do
+ let_it_be(:admin) { create(:user, :admin) }
+ let_it_be(:organization_user) { create(:organization_user) }
+ let_it_be(:organization) { organization_user.organization }
+ let_it_be(:another_organization) { create(:organization) }
+ let_it_be(:another_user) { create(:user) }
+
+ let(:current_user) { organization_user.user }
+ let(:target_user) { organization_user.user }
+
+ subject(:finder) { described_class.new(current_user, target_user).execute }
+
+ context 'when the current user has access to the organization' do
+ it { is_expected.to contain_exactly(organization) }
+ end
+
+ context 'when the current user is an admin' do
+ let(:current_user) { admin }
+
+ context 'when admin mode is enabled', :enable_admin_mode do
+ it { is_expected.to contain_exactly(organization) }
+ end
+
+ context 'when admin mode is disabled' do
+ it { is_expected.to be_empty }
+ end
+ end
+
+ context 'when the current user does not access to the organization' do
+ let(:current_user) { another_user }
+
+ it { is_expected.to be_empty }
+ end
+
+ context 'when the current user is nil' do
+ let(:current_user) { nil }
+
+ it { is_expected.to be_empty }
+ end
+
+ context 'when the target user is nil' do
+ let(:target_user) { nil }
+
+ it { is_expected.to be_empty }
+ end
+end
diff --git a/spec/finders/packages/group_packages_finder_spec.rb b/spec/finders/packages/group_packages_finder_spec.rb
index e4a944eb837..a2698bc0153 100644
--- a/spec/finders/packages/group_packages_finder_spec.rb
+++ b/spec/finders/packages/group_packages_finder_spec.rb
@@ -203,7 +203,7 @@ RSpec.describe Packages::GroupPackagesFinder do
end
context 'group has package of all types' do
- package_types.each do |pt| # rubocop:disable RSpec/UselessDynamicDefinition
+ package_types.each do |pt| # rubocop:disable RSpec/UselessDynamicDefinition -- `pt` used in `let`
let_it_be("package_#{pt}") { create("#{pt}_package", project: project) }
end
diff --git a/spec/finders/packages/pypi/packages_finder_spec.rb b/spec/finders/packages/pypi/packages_finder_spec.rb
index 3957eb188da..26cfaa29a0c 100644
--- a/spec/finders/packages/pypi/packages_finder_spec.rb
+++ b/spec/finders/packages/pypi/packages_finder_spec.rb
@@ -64,6 +64,16 @@ RSpec.describe Packages::Pypi::PackagesFinder do
end
it { is_expected.to contain_exactly(package2, package3, package4) }
+
+ context 'when package registry is disabled for one project' do
+ before do
+ project2.project_feature.update!(package_registry_access_level: ProjectFeature::DISABLED)
+ end
+
+ it 'filters the packages from the disabled project' do
+ expect(subject).to contain_exactly(package2, package3)
+ end
+ end
end
end
end
diff --git a/spec/finders/personal_access_tokens_finder_spec.rb b/spec/finders/personal_access_tokens_finder_spec.rb
index d91b2c8f599..d7cc72fe8ed 100644
--- a/spec/finders/personal_access_tokens_finder_spec.rb
+++ b/spec/finders/personal_access_tokens_finder_spec.rb
@@ -2,16 +2,16 @@
require 'spec_helper'
-RSpec.describe PersonalAccessTokensFinder, :enable_admin_mode do
+RSpec.describe PersonalAccessTokensFinder, :enable_admin_mode, feature_category: :system_access do
using RSpec::Parameterized::TableSyntax
describe '#execute' do
- let(:admin) { create(:admin) }
- let(:user) { create(:user) }
- let(:other_user) { create(:user) }
- let(:project_bot) { create(:user, :project_bot) }
+ let_it_be(:admin) { create(:admin) }
+ let_it_be(:user) { create(:user) }
+ let_it_be(:other_user) { create(:user) }
+ let_it_be(:project_bot) { create(:user, :project_bot) }
- let!(:tokens) do
+ let_it_be(:tokens) do
{
active: create(:personal_access_token, user: user, name: 'my_pat_1'),
active_other: create(:personal_access_token, user: other_user, name: 'my_pat_2'),
@@ -24,6 +24,8 @@ RSpec.describe PersonalAccessTokensFinder, :enable_admin_mode do
}
end
+ let(:tokens_keys) { tokens.keys }
+
let(:params) { {} }
let(:current_user) { admin }
@@ -89,7 +91,7 @@ RSpec.describe PersonalAccessTokensFinder, :enable_admin_mode do
describe 'by user' do
where(:by_user, :expected_tokens) do
- nil | tokens.keys
+ nil | ref(:tokens_keys)
ref(:user) | [:active, :expired, :revoked, :active_impersonation, :expired_impersonation, :revoked_impersonation]
ref(:other_user) | [:active_other]
ref(:admin) | []
@@ -106,7 +108,7 @@ RSpec.describe PersonalAccessTokensFinder, :enable_admin_mode do
describe 'by users' do
where(:by_users, :expected_tokens) do
- nil | tokens.keys
+ nil | ref(:tokens_keys)
lazy { [user] } | [:active, :expired, :revoked, :active_impersonation, :expired_impersonation, :revoked_impersonation]
lazy { [other_user] } | [:active_other]
lazy { [user, other_user] } | [:active, :active_other, :expired, :revoked, :active_impersonation, :expired_impersonation, :revoked_impersonation]
@@ -124,10 +126,10 @@ RSpec.describe PersonalAccessTokensFinder, :enable_admin_mode do
describe 'by impersonation' do
where(:by_impersonation, :expected_tokens) do
- nil | tokens.keys
+ nil | ref(:tokens_keys)
true | [:active_impersonation, :expired_impersonation, :revoked_impersonation]
false | [:active, :active_other, :expired, :revoked, :bot]
- 'other' | tokens.keys
+ 'other' | ref(:tokens_keys)
end
with_them do
@@ -141,10 +143,10 @@ RSpec.describe PersonalAccessTokensFinder, :enable_admin_mode do
describe 'by state' do
where(:by_state, :expected_tokens) do
- nil | tokens.keys
+ nil | ref(:tokens_keys)
'active' | [:active, :active_other, :active_impersonation, :bot]
'inactive' | [:expired, :revoked, :expired_impersonation, :revoked_impersonation]
- 'other' | tokens.keys
+ 'other' | ref(:tokens_keys)
end
with_them do
@@ -158,9 +160,9 @@ RSpec.describe PersonalAccessTokensFinder, :enable_admin_mode do
describe 'by owner type' do
where(:by_owner_type, :expected_tokens) do
- nil | tokens.keys
+ nil | ref(:tokens_keys)
'human' | [:active, :active_other, :expired, :revoked, :active_impersonation, :expired_impersonation, :revoked_impersonation]
- 'other' | tokens.keys
+ 'other' | ref(:tokens_keys)
end
with_them do
@@ -197,7 +199,7 @@ RSpec.describe PersonalAccessTokensFinder, :enable_admin_mode do
where(:by_created_before, :expected_tokens) do
6.days.ago | []
2.days.ago | [:active_other]
- 2.days.from_now | tokens.keys
+ 2.days.from_now | ref(:tokens_keys)
end
with_them do
@@ -211,7 +213,7 @@ RSpec.describe PersonalAccessTokensFinder, :enable_admin_mode do
describe 'by created after' do
where(:by_created_after, :expected_tokens) do
- 6.days.ago | tokens.keys
+ 6.days.ago | ref(:tokens_keys)
2.days.ago | [:active, :expired, :revoked, :active_impersonation, :expired_impersonation, :revoked_impersonation, :bot]
2.days.from_now | []
end
@@ -236,7 +238,7 @@ RSpec.describe PersonalAccessTokensFinder, :enable_admin_mode do
where(:by_last_used_before, :expected_tokens) do
6.days.ago | []
2.days.ago | [:active_other]
- 2.days.from_now | tokens.keys
+ 2.days.from_now | ref(:tokens_keys)
end
with_them do
@@ -250,7 +252,7 @@ RSpec.describe PersonalAccessTokensFinder, :enable_admin_mode do
describe 'by last used after' do
where(:by_last_used_after, :expected_tokens) do
- 6.days.ago | tokens.keys
+ 6.days.ago | ref(:tokens_keys)
2.days.ago | [:active, :expired, :revoked, :active_impersonation, :expired_impersonation, :revoked_impersonation, :bot]
2.days.from_now | []
end
@@ -267,7 +269,7 @@ RSpec.describe PersonalAccessTokensFinder, :enable_admin_mode do
describe 'by search' do
where(:by_search, :expected_tokens) do
- nil | tokens.keys
+ nil | ref(:tokens_keys)
'my_pat' | [:active, :active_other]
'other' | []
end
@@ -283,10 +285,10 @@ RSpec.describe PersonalAccessTokensFinder, :enable_admin_mode do
describe 'sort' do
where(:sort, :expected_tokens) do
- nil | tokens.keys
+ nil | ref(:tokens_keys)
'id_asc' | [:active, :active_other, :expired, :revoked, :active_impersonation, :expired_impersonation, :revoked_impersonation, :bot]
'id_desc' | [:bot, :revoked_impersonation, :expired_impersonation, :active_impersonation, :revoked, :expired, :active_other, :active]
- 'other' | tokens.keys
+ 'other' | ref(:tokens_keys)
end
with_them do
diff --git a/spec/finders/projects/ml/model_finder_spec.rb b/spec/finders/projects/ml/model_finder_spec.rb
index 1d869e1792d..a2c2836a63d 100644
--- a/spec/finders/projects/ml/model_finder_spec.rb
+++ b/spec/finders/projects/ml/model_finder_spec.rb
@@ -6,24 +6,57 @@ RSpec.describe Projects::Ml::ModelFinder, feature_category: :mlops do
let_it_be(:project) { create(:project) }
let_it_be(:model1) { create(:ml_models, :with_versions, project: project) }
let_it_be(:model2) { create(:ml_models, :with_versions, project: project) }
- let_it_be(:model3) { create(:ml_models) }
+ let_it_be(:model3) { create(:ml_models, name: "#{model1.name}_1", project: project) }
+ let_it_be(:other_model) { create(:ml_models) }
+ let_it_be(:project_models) { [model1, model2, model3] }
- subject(:models) { described_class.new(project).execute.to_a }
+ let(:params) { {} }
- it 'returns models for project' do
- is_expected.to match_array([model1, model2])
- end
+ subject(:models) { described_class.new(project, params).execute.to_a }
+
+ describe 'default params' do
+ it 'returns models for project ordered by id' do
+ is_expected.to eq([model3, model2, model1])
+ end
+
+ it 'including the latest version and project', :aggregate_failures do
+ expect(models[0].association_cached?(:latest_version)).to be(true)
+ expect(models[0].association_cached?(:project)).to be(true)
+ expect(models[1].association_cached?(:latest_version)).to be(true)
+ expect(models[1].association_cached?(:project)).to be(true)
+ end
+
+ it 'does not return models belonging to a different project' do
+ is_expected.not_to include(other_model)
+ end
- it 'including the latest version', :aggregate_failures do
- expect(models[0].association_cached?(:latest_version)).to be(true)
- expect(models[1].association_cached?(:latest_version)).to be(true)
+ it 'includes version count' do
+ expect(models[0].version_count).to be(models[0].versions.count)
+ end
end
- it 'does not return models belonging to a different project' do
- is_expected.not_to include(model3)
+ context 'when name is passed' do
+ let(:params) { { name: model1.name } }
+
+ it 'searches by name' do
+ is_expected.to match_array([model1, model3])
+ end
end
- it 'includes version count' do
- expect(models[0].version_count).to be(models[0].versions.count)
+ describe 'sorting' do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:test_case, :order_by, :direction, :expected_order) do
+ 'default params' | nil | nil | [2, 1, 0]
+ 'ascending order' | 'id' | 'ASC' | [0, 1, 2]
+ 'by column' | 'name' | 'ASC' | [0, 2, 1]
+ 'invalid sort' | nil | 'UP' | [2, 1, 0]
+ 'invalid order by' | 'INVALID' | nil | [2, 1, 0]
+ end
+ with_them do
+ let(:params) { { order_by: order_by, sort: direction } }
+
+ it { expect(subject).to eq(project_models.values_at(*expected_order)) }
+ end
end
end
diff --git a/spec/finders/projects_finder_spec.rb b/spec/finders/projects_finder_spec.rb
index a795df4dec6..f7afd96fa09 100644
--- a/spec/finders/projects_finder_spec.rb
+++ b/spec/finders/projects_finder_spec.rb
@@ -66,6 +66,18 @@ RSpec.describe ProjectsFinder, feature_category: :groups_and_projects do
it { is_expected.to eq([internal_project]) }
end
+ describe 'with full_paths' do
+ let_it_be(:second_public_project) do
+ create(:project, :public, :merge_requests_enabled, :issues_disabled, group: group, name: 'second-public', path: 'second-public')
+ end
+
+ context 'only returns projects matching the provided full paths' do
+ let(:params) { { full_paths: [public_project.full_path, second_public_project.full_path] } }
+
+ it { is_expected.to match_array([public_project, second_public_project]) }
+ end
+ end
+
describe 'with id_after' do
context 'only returns projects with a project id greater than given' do
let(:params) { { id_after: internal_project.id } }
@@ -413,13 +425,30 @@ RSpec.describe ProjectsFinder, feature_category: :groups_and_projects do
it { is_expected.to match_array([internal_project]) }
end
- describe 'always filters by without_deleted' do
+ describe 'filters by without_deleted by default' do
let_it_be(:pending_delete_project) { create(:project, :public, pending_delete: true) }
it 'returns projects that are not pending_delete' do
expect(subject).not_to include(pending_delete_project)
expect(subject).to include(public_project, internal_project)
end
+
+ context 'when include_pending_delete param is provided' do
+ let(:params) { { include_pending_delete: true } }
+
+ it 'returns projects that are not pending_delete' do
+ expect(subject).not_to include(pending_delete_project)
+ expect(subject).to include(public_project, internal_project)
+ end
+
+ context 'when user is an admin', :enable_admin_mode do
+ let(:current_user) { create(:admin) }
+
+ it 'also return pending_delete projects' do
+ expect(subject).to include(public_project, internal_project, pending_delete_project)
+ end
+ end
+ end
end
describe 'filter by last_activity_before' do
diff --git a/spec/finders/user_group_notification_settings_finder_spec.rb b/spec/finders/user_group_notification_settings_finder_spec.rb
index ac59a42d813..83d0d343c04 100644
--- a/spec/finders/user_group_notification_settings_finder_spec.rb
+++ b/spec/finders/user_group_notification_settings_finder_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe UserGroupNotificationSettingsFinder do
+RSpec.describe UserGroupNotificationSettingsFinder, feature_category: :team_planning do
let_it_be(:user) { create(:user) }
subject { described_class.new(user, Group.where(id: groups.map(&:id))).execute }
@@ -127,38 +127,38 @@ RSpec.describe UserGroupNotificationSettingsFinder do
expect(result.count).to eq(3)
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 'preloading `emails_enabled`' 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(:another_root_group) { create(:group) }
+ let_it_be(:sub_group_with_emails_disabled) { create(:group, emails_enabled: false, 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_it_be(:root_group_with_emails_disabled) { create(:group, emails_enabled: false) }
+ 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]) }
+ let(:groups) { Group.where(id: [sub_sub_group, another_sub_sub_group, group]) }
- before do
- described_class.new(user, groups).execute
- end
+ before do
+ 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
+ it 'preloads the `group.emails_enabled` method' do
+ recorder = ActiveRecord::QueryRecorder.new do
+ groups.each(&:emails_enabled?)
+ end
- expect(recorder.count).to eq(0)
- end
+ expect(recorder.count).to eq(0)
+ end
- 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
+ it 'preloads the `group.emails_enabled` method correctly' do
+ groups.each do |group|
+ expect(group.emails_enabled?).to eq(Group.find(group.id).emails_enabled?) # compare the memoized and the freshly loaded value
+ end
+ end
end
end
end
diff --git a/spec/finders/vs_code/settings/settings_finder_spec.rb b/spec/finders/vs_code/settings/settings_finder_spec.rb
index b7b4308bbbd..fa24f5d0aec 100644
--- a/spec/finders/vs_code/settings/settings_finder_spec.rb
+++ b/spec/finders/vs_code/settings/settings_finder_spec.rb
@@ -34,8 +34,6 @@ RSpec.describe VsCode::Settings::SettingsFinder, feature_category: :web_ide do
let_it_be(:setting) { create(:vscode_setting, user: user) }
context 'when user has no settings with that type' do
- subject { finder.execute }
-
it 'returns an empty array' do
finder = described_class.new(user, ['profile'])
expect(finder.execute).to eq([])
@@ -43,8 +41,6 @@ RSpec.describe VsCode::Settings::SettingsFinder, feature_category: :web_ide do
end
context 'when user does have settings with the type' do
- subject { finder.execute }
-
it 'returns the record when a single setting exists' do
result = described_class.new(user, ['settings']).execute
expect(result.length).to eq(1)
@@ -53,9 +49,9 @@ RSpec.describe VsCode::Settings::SettingsFinder, feature_category: :web_ide do
end
it 'returns multiple records when more than one setting exists' do
- create(:vscode_setting, user: user, setting_type: 'profile')
+ create(:vscode_setting, user: user, setting_type: 'globalState')
- result = described_class.new(user, %w[settings profile]).execute
+ result = described_class.new(user, %w[settings globalState]).execute
expect(result.length).to eq(2)
end
end
diff --git a/spec/fixtures/api/schemas/entities/admin_users_data_attributes_paths.json b/spec/fixtures/api/schemas/entities/admin_users_data_attributes_paths.json
index 44d8e48a972..61472b273e1 100644
--- a/spec/fixtures/api/schemas/entities/admin_users_data_attributes_paths.json
+++ b/spec/fixtures/api/schemas/entities/admin_users_data_attributes_paths.json
@@ -1,19 +1,51 @@
{
"type": "object",
"properties": {
- "edit": { "type": "string" },
- "approve": { "type": "string" },
- "reject": { "type": "string" },
- "unblock": { "type": "string" },
- "block": { "type": "string" },
- "deactivate": { "type": "string" },
- "activate": { "type": "string" },
- "unlock": { "type": "string" },
- "delete": { "type": "string" },
- "delete_with_contributions": { "type": "string" },
- "admin_user": { "type": "string" },
- "ban": { "type": "string" },
- "unban": { "type": "string" }
+ "edit": {
+ "type": "string"
+ },
+ "approve": {
+ "type": "string"
+ },
+ "reject": {
+ "type": "string"
+ },
+ "unblock": {
+ "type": "string"
+ },
+ "block": {
+ "type": "string"
+ },
+ "deactivate": {
+ "type": "string"
+ },
+ "activate": {
+ "type": "string"
+ },
+ "unlock": {
+ "type": "string"
+ },
+ "delete": {
+ "type": "string"
+ },
+ "delete_with_contributions": {
+ "type": "string"
+ },
+ "admin_user": {
+ "type": "string"
+ },
+ "ban": {
+ "type": "string"
+ },
+ "unban": {
+ "type": "string"
+ },
+ "trust": {
+ "type": "string"
+ },
+ "untrust": {
+ "type": "string"
+ }
},
"required": [
"edit",
@@ -28,7 +60,9 @@
"delete_with_contributions",
"admin_user",
"ban",
- "unban"
+ "unban",
+ "trust",
+ "untrust"
],
"additionalProperties": false
}
diff --git a/spec/fixtures/api/schemas/entities/member.json b/spec/fixtures/api/schemas/entities/member.json
index cd8a4e0519b..38f8a245b49 100644
--- a/spec/fixtures/api/schemas/entities/member.json
+++ b/spec/fixtures/api/schemas/entities/member.json
@@ -11,7 +11,8 @@
"type",
"can_update",
"can_remove",
- "is_direct_member"
+ "is_direct_member",
+ "custom_roles"
],
"properties": {
"id": {
@@ -48,7 +49,8 @@
"type": "object",
"required": [
"integer_value",
- "string_value"
+ "string_value",
+ "member_role_id"
],
"properties": {
"integer_value": {
@@ -56,6 +58,12 @@
},
"string_value": {
"type": "string"
+ },
+ "member_role_id": {
+ "type": [
+ "integer",
+ "null"
+ ]
}
},
"additionalProperties": false
@@ -138,6 +146,26 @@
}
},
"additionalProperties": false
+ },
+ "custom_roles": {
+ "type": "array",
+ "items": [
+ {
+ "type": "object",
+ "properties": {
+ "base_access_level": {
+ "type": "integer"
+ },
+ "member_role_id": {
+ "type": "integer"
+ },
+ "name": {
+ "type": "string"
+ }
+ },
+ "additionalProperties": false
+ }
+ ]
}
}
-} \ No newline at end of file
+}
diff --git a/spec/fixtures/api/schemas/entities/merge_request_noteable.json b/spec/fixtures/api/schemas/entities/merge_request_noteable.json
index 4b790a2c34b..6f3c29b16e9 100644
--- a/spec/fixtures/api/schemas/entities/merge_request_noteable.json
+++ b/spec/fixtures/api/schemas/entities/merge_request_noteable.json
@@ -24,13 +24,11 @@
"type": "object",
"required": [
"can_create_note",
- "can_update",
- "can_approve"
+ "can_update"
],
"properties": {
"can_create_note": { "type": "boolean" },
- "can_update": { "type": "boolean" },
- "can_approve": { "type": "boolean" }
+ "can_update": { "type": "boolean" }
},
"additionalProperties": false
},
diff --git a/spec/fixtures/api/schemas/graphql/packages/package_details.json b/spec/fixtures/api/schemas/graphql/packages/package_details.json
index 2e7a950d330..1acc3f6ad7d 100644
--- a/spec/fixtures/api/schemas/graphql/packages/package_details.json
+++ b/spec/fixtures/api/schemas/graphql/packages/package_details.json
@@ -17,7 +17,8 @@
"statusMessage",
"canDestroy",
"lastDownloadedAt",
- "_links"
+ "_links",
+ "userPermissions"
],
"properties": {
"id": {
@@ -272,6 +273,15 @@
]
}
}
+ },
+ "userPermissions": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "destroyPackage": {
+ "type": "boolean"
+ }
+ }
}
}
}
diff --git a/spec/fixtures/api/schemas/graphql/packages/package_pypi_metadata.json b/spec/fixtures/api/schemas/graphql/packages/package_pypi_metadata.json
index cecebe3a0e9..c9b941ed8fa 100644
--- a/spec/fixtures/api/schemas/graphql/packages/package_pypi_metadata.json
+++ b/spec/fixtures/api/schemas/graphql/packages/package_pypi_metadata.json
@@ -1,13 +1,51 @@
{
"type": "object",
"additionalProperties": false,
- "required": ["id"],
+ "required": [
+ "id"
+ ],
"properties": {
"id": {
"type": "string"
},
+ "authorEmail": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "description": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "descriptionContentType": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "keywords": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "metadataVersion": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
"requiredPython": {
"type": "string"
+ },
+ "summary": {
+ "type": [
+ "string",
+ "null"
+ ]
}
}
}
diff --git a/spec/fixtures/api/schemas/group_link/group_link.json b/spec/fixtures/api/schemas/group_link/group_link.json
index 885ed6d18e0..4db38952ecc 100644
--- a/spec/fixtures/api/schemas/group_link/group_link.json
+++ b/spec/fixtures/api/schemas/group_link/group_link.json
@@ -44,6 +44,9 @@
"valid_roles": {
"type": "object"
},
+ "is_shared_with_group_private": {
+ "type": "boolean"
+ },
"shared_with_group": {
"type": "object",
"required": [
@@ -89,4 +92,4 @@
"type": "boolean"
}
}
-} \ No newline at end of file
+}
diff --git a/spec/fixtures/api/schemas/job/test_report_summary.json b/spec/fixtures/api/schemas/job/test_report_summary.json
new file mode 100644
index 00000000000..056a02854d4
--- /dev/null
+++ b/spec/fixtures/api/schemas/job/test_report_summary.json
@@ -0,0 +1,34 @@
+{
+ "type": "object",
+ "properties": {
+ "total": {
+ "type": "object",
+ "properties": {
+ "time": { "type": "number" },
+ "count": { "type": "number" },
+ "success": { "type": "number" },
+ "failed": { "type": "number" },
+ "skipped": { "type": "number" },
+ "error": { "type": "number" },
+ "suite_error": { "type": ["string", "null"] }
+ }
+ },
+ "test_suites": {
+ "type": "array",
+ "items": {
+ "name": { "type": "string" },
+ "total_time": { "type": "number" },
+ "total_count": { "type": "number" },
+ "success_count": { "type": "number" },
+ "failed_count": { "type": "number" },
+ "skipped_count": { "type": "number" },
+ "error_count": { "type": "number" },
+ "build_ids": {
+ "type": "array",
+ "items": { "type": "number" }
+ },
+ "suite_error": { "type": ["string", "null"] }
+ }
+ }
+ }
+}
diff --git a/spec/fixtures/api/schemas/ml/get_latest_versions.json b/spec/fixtures/api/schemas/ml/get_latest_versions.json
new file mode 100644
index 00000000000..cb2308fa637
--- /dev/null
+++ b/spec/fixtures/api/schemas/ml/get_latest_versions.json
@@ -0,0 +1,80 @@
+{
+ "type": "object",
+ "required": [
+ "model_versions"
+ ],
+ "properties": {
+ "model_versions": {
+ "type": "array",
+ "items": [
+ {
+ "type": "object",
+ "required": [
+ "name",
+ "version",
+ "creation_timestamp",
+ "last_updated_timestamp",
+ "user_id",
+ "current_stage",
+ "description",
+ "source",
+ "run_id",
+ "status",
+ "status_message",
+ "metadata",
+ "run_link",
+ "aliases"
+ ],
+ "properties": {
+ "name": {
+ "type": "string"
+ },
+ "version": {
+ "type": "string"
+ },
+ "creation_timestamp": {
+ "type": "integer"
+ },
+ "last_updated_timestamp": {
+ "type": "integer"
+ },
+ "user_id": {
+ "type": "null"
+ },
+ "current_stage": {
+ "type": "string"
+ },
+ "description": {
+ "type": "string"
+ },
+ "source": {
+ "type": "string"
+ },
+ "run_id": {
+ "type": "string"
+ },
+ "status": {
+ "type": "string"
+ },
+ "status_message": {
+ "type": "string"
+ },
+ "metadata": {
+ "type": "array",
+ "items": {
+ }
+ },
+ "run_link": {
+ "type": "string"
+ },
+ "aliases": {
+ "type": "array",
+ "items": {
+ }
+ }
+ }
+ }
+ ]
+ }
+ }
+}
diff --git a/spec/fixtures/api/schemas/ml/get_model.json b/spec/fixtures/api/schemas/ml/get_model.json
new file mode 100644
index 00000000000..6b7ced6845b
--- /dev/null
+++ b/spec/fixtures/api/schemas/ml/get_model.json
@@ -0,0 +1,51 @@
+{
+ "type": "object",
+ "required": [
+ "registered_model"
+ ],
+ "properties": {
+ "model": {
+ "type": "object",
+ "required": [
+ "name",
+ "description",
+ "user_id"
+ ],
+ "properties": {
+ "name": {
+ "type": "string"
+ },
+ "description": {
+ "type": "string"
+ },
+ "user_id": {
+ "type": "integer"
+ },
+ "creation_timestamp": {
+ "type": "string"
+ },
+ "last_updated_timestamp": {
+ "type": "string"
+ },
+ "tags": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "required": [
+ "key",
+ "value"
+ ],
+ "properties": {
+ "key": {
+ "type": "string"
+ },
+ "value": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/spec/fixtures/api/schemas/ml/update_model.json b/spec/fixtures/api/schemas/ml/update_model.json
new file mode 100644
index 00000000000..6b7ced6845b
--- /dev/null
+++ b/spec/fixtures/api/schemas/ml/update_model.json
@@ -0,0 +1,51 @@
+{
+ "type": "object",
+ "required": [
+ "registered_model"
+ ],
+ "properties": {
+ "model": {
+ "type": "object",
+ "required": [
+ "name",
+ "description",
+ "user_id"
+ ],
+ "properties": {
+ "name": {
+ "type": "string"
+ },
+ "description": {
+ "type": "string"
+ },
+ "user_id": {
+ "type": "integer"
+ },
+ "creation_timestamp": {
+ "type": "string"
+ },
+ "last_updated_timestamp": {
+ "type": "string"
+ },
+ "tags": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "required": [
+ "key",
+ "value"
+ ],
+ "properties": {
+ "key": {
+ "type": "string"
+ },
+ "value": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/spec/fixtures/click_house/migrations/drop_table/1_create_some_table.rb b/spec/fixtures/click_house/migrations/drop_table/1_create_some_table.rb
new file mode 100644
index 00000000000..14ef80cbdb7
--- /dev/null
+++ b/spec/fixtures/click_house/migrations/drop_table/1_create_some_table.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+# rubocop: disable Gitlab/NamespacedClass -- Fixtures do not need to be namespaced
+class CreateSomeTable < ClickHouse::Migration
+ def up
+ execute <<~SQL
+ CREATE TABLE some (
+ id UInt64,
+ date Date
+ ) ENGINE = Memory
+ SQL
+ end
+end
+# rubocop: enable Gitlab/NamespacedClass
diff --git a/spec/fixtures/click_house/migrations/drop_table/2_drop_some_table.rb b/spec/fixtures/click_house/migrations/drop_table/2_drop_some_table.rb
new file mode 100644
index 00000000000..82045b08e21
--- /dev/null
+++ b/spec/fixtures/click_house/migrations/drop_table/2_drop_some_table.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+# rubocop: disable Gitlab/NamespacedClass -- Fixtures do not need to be namespaced
+class DropSomeTable < ClickHouse::Migration
+ def up
+ execute <<~SQL
+ DROP TABLE some
+ SQL
+ end
+end
+# rubocop: enable Gitlab/NamespacedClass
diff --git a/spec/fixtures/click_house/migrations/duplicate_name/1_create_some_table.rb b/spec/fixtures/click_house/migrations/duplicate_name/1_create_some_table.rb
new file mode 100644
index 00000000000..14ef80cbdb7
--- /dev/null
+++ b/spec/fixtures/click_house/migrations/duplicate_name/1_create_some_table.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+# rubocop: disable Gitlab/NamespacedClass -- Fixtures do not need to be namespaced
+class CreateSomeTable < ClickHouse::Migration
+ def up
+ execute <<~SQL
+ CREATE TABLE some (
+ id UInt64,
+ date Date
+ ) ENGINE = Memory
+ SQL
+ end
+end
+# rubocop: enable Gitlab/NamespacedClass
diff --git a/spec/fixtures/click_house/migrations/duplicate_name/2_create_some_table.rb b/spec/fixtures/click_house/migrations/duplicate_name/2_create_some_table.rb
new file mode 100644
index 00000000000..be6c1905502
--- /dev/null
+++ b/spec/fixtures/click_house/migrations/duplicate_name/2_create_some_table.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+# rubocop: disable Gitlab/NamespacedClass -- Fixtures do not need to be namespaced
+class CreateSomeTable2 < ClickHouse::Migration
+ def up
+ execute <<~SQL
+ CREATE TABLE some (
+ id UInt64,
+ date Date
+ ) ENGINE = Memory
+ SQL
+ end
+end
+# rubocop: enable Gitlab/NamespacedClass
diff --git a/spec/fixtures/click_house/migrations/duplicate_version/1_create_some_table.rb b/spec/fixtures/click_house/migrations/duplicate_version/1_create_some_table.rb
new file mode 100644
index 00000000000..14ef80cbdb7
--- /dev/null
+++ b/spec/fixtures/click_house/migrations/duplicate_version/1_create_some_table.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+# rubocop: disable Gitlab/NamespacedClass -- Fixtures do not need to be namespaced
+class CreateSomeTable < ClickHouse::Migration
+ def up
+ execute <<~SQL
+ CREATE TABLE some (
+ id UInt64,
+ date Date
+ ) ENGINE = Memory
+ SQL
+ end
+end
+# rubocop: enable Gitlab/NamespacedClass
diff --git a/spec/fixtures/click_house/migrations/duplicate_version/1_drop_some_table.rb b/spec/fixtures/click_house/migrations/duplicate_version/1_drop_some_table.rb
new file mode 100644
index 00000000000..82045b08e21
--- /dev/null
+++ b/spec/fixtures/click_house/migrations/duplicate_version/1_drop_some_table.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+# rubocop: disable Gitlab/NamespacedClass -- Fixtures do not need to be namespaced
+class DropSomeTable < ClickHouse::Migration
+ def up
+ execute <<~SQL
+ DROP TABLE some
+ SQL
+ end
+end
+# rubocop: enable Gitlab/NamespacedClass
diff --git a/spec/fixtures/click_house/migrations/migration_with_error/1_migration_with_error.rb b/spec/fixtures/click_house/migrations/migration_with_error/1_migration_with_error.rb
new file mode 100644
index 00000000000..b8ae3df2085
--- /dev/null
+++ b/spec/fixtures/click_house/migrations/migration_with_error/1_migration_with_error.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+# rubocop: disable Gitlab/NamespacedClass -- Fixtures do not need to be namespaced
+class MigrationWithError < ClickHouse::Migration
+ def up
+ raise ClickHouse::Client::DatabaseError, 'A migration error happened'
+ end
+end
+# rubocop: enable Gitlab/NamespacedClass
diff --git a/spec/fixtures/click_house/migrations/migrations_over_multiple_databases/1_create_some_table_on_main_db.rb b/spec/fixtures/click_house/migrations/migrations_over_multiple_databases/1_create_some_table_on_main_db.rb
new file mode 100644
index 00000000000..98d71d9507b
--- /dev/null
+++ b/spec/fixtures/click_house/migrations/migrations_over_multiple_databases/1_create_some_table_on_main_db.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+# rubocop: disable Gitlab/NamespacedClass -- Fixtures do not need to be namespaced
+class CreateSomeTableOnMainDb < ClickHouse::Migration
+ def up
+ execute <<~SQL
+ CREATE TABLE some (
+ id UInt64,
+ date Date
+ ) ENGINE = MergeTree
+ PRIMARY KEY(id)
+ SQL
+ end
+end
+# rubocop: enable Gitlab/NamespacedClass
diff --git a/spec/fixtures/click_house/migrations/migrations_over_multiple_databases/2_create_some_table_on_another_db.rb b/spec/fixtures/click_house/migrations/migrations_over_multiple_databases/2_create_some_table_on_another_db.rb
new file mode 100644
index 00000000000..b8cd86a67f5
--- /dev/null
+++ b/spec/fixtures/click_house/migrations/migrations_over_multiple_databases/2_create_some_table_on_another_db.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+# rubocop: disable Gitlab/NamespacedClass -- Fixtures do not need to be namespaced
+class CreateSomeTableOnAnotherDb < ClickHouse::Migration
+ SCHEMA = :another_db
+
+ def up
+ execute <<~SQL
+ CREATE TABLE some_on_another_db (
+ id UInt64,
+ date Date
+ ) ENGINE = Memory
+ SQL
+ end
+end
+# rubocop: enable Gitlab/NamespacedClass
diff --git a/spec/fixtures/click_house/migrations/migrations_over_multiple_databases/3_change_some_table_on_main_db.rb b/spec/fixtures/click_house/migrations/migrations_over_multiple_databases/3_change_some_table_on_main_db.rb
new file mode 100644
index 00000000000..9112ab79fc5
--- /dev/null
+++ b/spec/fixtures/click_house/migrations/migrations_over_multiple_databases/3_change_some_table_on_main_db.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+# rubocop: disable Gitlab/NamespacedClass -- Fixtures do not need to be namespaced
+class ChangeSomeTableOnMainDb < ClickHouse::Migration
+ def up
+ execute <<~SQL
+ ALTER TABLE some RENAME COLUMN date to timestamp
+ SQL
+ end
+end
+# rubocop: enable Gitlab/NamespacedClass
diff --git a/spec/fixtures/click_house/migrations/plain_table_creation/1_create_some_table.rb b/spec/fixtures/click_house/migrations/plain_table_creation/1_create_some_table.rb
new file mode 100644
index 00000000000..14ef80cbdb7
--- /dev/null
+++ b/spec/fixtures/click_house/migrations/plain_table_creation/1_create_some_table.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+# rubocop: disable Gitlab/NamespacedClass -- Fixtures do not need to be namespaced
+class CreateSomeTable < ClickHouse::Migration
+ def up
+ execute <<~SQL
+ CREATE TABLE some (
+ id UInt64,
+ date Date
+ ) ENGINE = Memory
+ SQL
+ end
+end
+# rubocop: enable Gitlab/NamespacedClass
diff --git a/spec/fixtures/click_house/migrations/plain_table_creation_on_invalid_database/1_create_some_table.rb b/spec/fixtures/click_house/migrations/plain_table_creation_on_invalid_database/1_create_some_table.rb
new file mode 100644
index 00000000000..ee900ef24c5
--- /dev/null
+++ b/spec/fixtures/click_house/migrations/plain_table_creation_on_invalid_database/1_create_some_table.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+# rubocop: disable Gitlab/NamespacedClass -- Fixtures do not need to be namespaced
+class CreateSomeTable < ClickHouse::Migration
+ SCHEMA = :unknown_database
+
+ def up
+ execute <<~SQL
+ CREATE TABLE some (
+ id UInt64,
+ date Date
+ ) ENGINE = Memory
+ SQL
+ end
+end
+# rubocop: enable Gitlab/NamespacedClass
diff --git a/spec/fixtures/click_house/migrations/table_creation_with_down_method/1_create_some_table.rb b/spec/fixtures/click_house/migrations/table_creation_with_down_method/1_create_some_table.rb
new file mode 100644
index 00000000000..7ac92b9ee38
--- /dev/null
+++ b/spec/fixtures/click_house/migrations/table_creation_with_down_method/1_create_some_table.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+# rubocop: disable Gitlab/NamespacedClass -- Fixtures do not need to be namespaced
+class CreateSomeTable < ClickHouse::Migration
+ def up
+ execute <<~SQL
+ CREATE TABLE some (
+ id UInt64,
+ date Date
+ ) ENGINE = Memory
+ SQL
+ end
+
+ def down
+ execute <<~SQL
+ DROP TABLE some
+ SQL
+ end
+end
+# rubocop: enable Gitlab/NamespacedClass
diff --git a/spec/fixtures/origin_cert_key.pem b/spec/fixtures/origin_cert_key.pem
new file mode 100644
index 00000000000..e69de29bb2d
--- /dev/null
+++ b/spec/fixtures/origin_cert_key.pem
diff --git a/spec/fixtures/security_reports/master/gl-common-scanning-report.json b/spec/fixtures/security_reports/master/gl-common-scanning-report.json
index 47e2a503b02..35db4779920 100644
--- a/spec/fixtures/security_reports/master/gl-common-scanning-report.json
+++ b/spec/fixtures/security_reports/master/gl-common-scanning-report.json
@@ -12,10 +12,10 @@
"id": "gemnasium",
"name": "Gemnasium"
},
- "cvss": [
+ "cvss_vectors": [
{
"vendor": "GitLab",
- "vector_string": "CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H"
+ "vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H"
}
],
"location": {
diff --git a/spec/fixtures/tooling/danger/rubocop_todo/cop1.yml b/spec/fixtures/tooling/danger/rubocop_todo/cop1.yml
new file mode 100644
index 00000000000..8f240b92682
--- /dev/null
+++ b/spec/fixtures/tooling/danger/rubocop_todo/cop1.yml
@@ -0,0 +1,5 @@
+---
+Cop1:
+ Exclude:
+ - 'app/controllers/application_controller.rb'
+ - 'app/controllers/acme_challenges_controller.rb'
diff --git a/spec/fixtures/tooling/danger/rubocop_todo/cop2.yml b/spec/fixtures/tooling/danger/rubocop_todo/cop2.yml
new file mode 100644
index 00000000000..9ab2c0dabb9
--- /dev/null
+++ b/spec/fixtures/tooling/danger/rubocop_todo/cop2.yml
@@ -0,0 +1,4 @@
+---
+Cop2:
+ Exclude:
+ - 'app/controllers/application_controller.rb'
diff --git a/spec/frontend/__helpers__/dom_shims/clipboard_event.js b/spec/frontend/__helpers__/dom_shims/clipboard_event.js
new file mode 100644
index 00000000000..fbff61503f2
--- /dev/null
+++ b/spec/frontend/__helpers__/dom_shims/clipboard_event.js
@@ -0,0 +1 @@
+window.ClipboardEvent = class ClipboardEvent extends Event {};
diff --git a/spec/frontend/__helpers__/dom_shims/drag_event.js b/spec/frontend/__helpers__/dom_shims/drag_event.js
new file mode 100644
index 00000000000..cbfee0b71ec
--- /dev/null
+++ b/spec/frontend/__helpers__/dom_shims/drag_event.js
@@ -0,0 +1 @@
+window.DragEvent = class DragEvent extends Event {};
diff --git a/spec/frontend/__helpers__/dom_shims/index.js b/spec/frontend/__helpers__/dom_shims/index.js
index 3b41e2ca2a7..f2c34b8ca31 100644
--- a/spec/frontend/__helpers__/dom_shims/index.js
+++ b/spec/frontend/__helpers__/dom_shims/index.js
@@ -1,5 +1,7 @@
import './clipboard';
+import './clipboard_event';
import './create_object_url';
+import './drag_event';
import './element_scroll_into_view';
import './element_scroll_by';
import './element_scroll_to';
diff --git a/spec/frontend/__helpers__/emoji.js b/spec/frontend/__helpers__/emoji.js
index 6c9291bdc8f..1037bd48df6 100644
--- a/spec/frontend/__helpers__/emoji.js
+++ b/spec/frontend/__helpers__/emoji.js
@@ -1,5 +1,5 @@
import { initEmojiMap, EMOJI_VERSION } from '~/emoji';
-import { CACHE_VERSION_KEY, CACHE_KEY } from '~/emoji/constants';
+import { CACHE_KEY } from '~/emoji/constants';
export const validEmoji = {
atom: {
@@ -95,19 +95,18 @@ export const emojiFixtureMap = {
export const mockEmojiData = Object.keys(emojiFixtureMap).reduce((acc, k) => {
const { moji: e, unicodeVersion: u, category: c, description: d } = emojiFixtureMap[k];
- acc[k] = { name: k, e, u, c, d };
+ acc.push({ n: k, e, u, c, d });
return acc;
-}, {});
+}, []);
export function clearEmojiMock() {
localStorage.clear();
initEmojiMap.promise = null;
}
-export async function initEmojiMock(mockData = mockEmojiData) {
+export async function initEmojiMock(data = mockEmojiData) {
clearEmojiMock();
- localStorage.setItem(CACHE_VERSION_KEY, EMOJI_VERSION);
- localStorage.setItem(CACHE_KEY, JSON.stringify(mockData));
+ localStorage.setItem(CACHE_KEY, JSON.stringify({ data, EMOJI_VERSION }));
await initEmojiMap();
}
diff --git a/spec/frontend/__helpers__/local_storage_helper.js b/spec/frontend/__helpers__/local_storage_helper.js
index cf75b0b53fe..367e7ec24ba 100644
--- a/spec/frontend/__helpers__/local_storage_helper.js
+++ b/spec/frontend/__helpers__/local_storage_helper.js
@@ -30,6 +30,9 @@ export const createLocalStorageSpy = () => {
let storage = {};
return {
+ get length() {
+ return Object.keys(storage).length;
+ },
clear: jest.fn(() => {
storage = {};
}),
diff --git a/spec/frontend/__helpers__/mock_observability_client.js b/spec/frontend/__helpers__/mock_observability_client.js
new file mode 100644
index 00000000000..82425aa2842
--- /dev/null
+++ b/spec/frontend/__helpers__/mock_observability_client.js
@@ -0,0 +1,19 @@
+import { buildClient } from '~/observability/client';
+
+export function createMockClient() {
+ const mockClient = buildClient({
+ provisioningUrl: 'provisioning-url',
+ tracingUrl: 'tracing-url',
+ servicesUrl: 'services-url',
+ operationsUrl: 'operations-url',
+ metricsUrl: 'metrics-url',
+ });
+
+ Object.getOwnPropertyNames(mockClient)
+ .filter((item) => typeof mockClient[item] === 'function')
+ .forEach((item) => {
+ mockClient[item] = jest.fn();
+ });
+
+ return mockClient;
+}
diff --git a/spec/frontend/__mocks__/@gitlab/ui.js b/spec/frontend/__mocks__/@gitlab/ui.js
index c51f37db384..04b3215a88a 100644
--- a/spec/frontend/__mocks__/@gitlab/ui.js
+++ b/spec/frontend/__mocks__/@gitlab/ui.js
@@ -1,3 +1,5 @@
+import '~/commons/gitlab_ui';
+
export * from '@gitlab/ui';
/**
diff --git a/spec/frontend/admin/abuse_report/components/abuse_report_app_spec.js b/spec/frontend/admin/abuse_report/components/abuse_report_app_spec.js
index 4340699a7ed..73e3f1eb49a 100644
--- a/spec/frontend/admin/abuse_report/components/abuse_report_app_spec.js
+++ b/spec/frontend/admin/abuse_report/components/abuse_report_app_spec.js
@@ -7,12 +7,14 @@ import ReportDetails from '~/admin/abuse_report/components/report_details.vue';
import ReportedContent from '~/admin/abuse_report/components/reported_content.vue';
import ActivityEventsList from '~/admin/abuse_report/components/activity_events_list.vue';
import ActivityHistoryItem from '~/admin/abuse_report/components/activity_history_item.vue';
+import AbuseReportNotes from '~/admin/abuse_report/components/abuse_report_notes.vue';
+
import { SUCCESS_ALERT } from '~/admin/abuse_report/constants';
import { mockAbuseReport } from '../mock_data';
describe('AbuseReportApp', () => {
let wrapper;
-
+ const mockAbuseReportId = mockAbuseReport.report.globalId;
const { similarOpenReports } = mockAbuseReport.user;
const findAlert = () => wrapper.findComponent(GlAlert);
@@ -27,6 +29,7 @@ describe('AbuseReportApp', () => {
const findActivityList = () => wrapper.findComponent(ActivityEventsList);
const findActivityItem = () => wrapper.findByTestId('activity');
+
const findActivityForSimilarReports = () =>
wrapper.findAllByTestId('activity-similar-open-reports');
const firstActivityForSimilarReports = () =>
@@ -34,6 +37,8 @@ describe('AbuseReportApp', () => {
const findReportDetails = () => wrapper.findComponent(ReportDetails);
+ const findAbuseReportNotes = () => wrapper.findComponent(AbuseReportNotes);
+
const createComponent = (props = {}, provide = {}) => {
wrapper = shallowMountExtended(AbuseReportApp, {
propsData: {
@@ -135,7 +140,7 @@ describe('AbuseReportApp', () => {
it('renders ReportDetails', () => {
createComponent({}, { glFeatures: { abuseReportLabels: true } });
- expect(findReportDetails().props('reportId')).toBe(mockAbuseReport.report.globalId);
+ expect(findReportDetails().props('reportId')).toBe(mockAbuseReportId);
});
});
@@ -162,4 +167,25 @@ describe('AbuseReportApp', () => {
expect(firstActivityForSimilarReports().props('report')).toBe(similarOpenReports[0]);
});
});
+
+ describe('Notes', () => {
+ describe('when abuseReportNotes feature flag is enabled', () => {
+ it('renders abuse report notes', () => {
+ createComponent({}, { glFeatures: { abuseReportNotes: true } });
+
+ expect(findAbuseReportNotes().exists()).toBe(true);
+ expect(findAbuseReportNotes().props()).toMatchObject({
+ abuseReportId: mockAbuseReportId,
+ });
+ });
+ });
+
+ describe('when abuseReportNotes feature flag is disabled', () => {
+ it('does not render ReportDetails', () => {
+ createComponent({}, { glFeatures: { abuseReportNotes: false } });
+
+ expect(findAbuseReportNotes().exists()).toBe(false);
+ });
+ });
+ });
});
diff --git a/spec/frontend/admin/abuse_report/components/abuse_report_notes_spec.js b/spec/frontend/admin/abuse_report/components/abuse_report_notes_spec.js
new file mode 100644
index 00000000000..166c735ffbd
--- /dev/null
+++ b/spec/frontend/admin/abuse_report/components/abuse_report_notes_spec.js
@@ -0,0 +1,98 @@
+import { shallowMount } from '@vue/test-utils';
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import { createAlert } from '~/alert';
+import SkeletonLoadingContainer from '~/vue_shared/components/notes/skeleton_note.vue';
+import abuseReportNotesQuery from '~/admin/abuse_report/graphql/notes/abuse_report_notes.query.graphql';
+import AbuseReportNotes from '~/admin/abuse_report/components/abuse_report_notes.vue';
+import AbuseReportDiscussion from '~/admin/abuse_report/components/notes/abuse_report_discussion.vue';
+
+import { mockAbuseReport, mockNotesByIdResponse } from '../mock_data';
+
+jest.mock('~/alert');
+
+describe('Abuse Report Notes', () => {
+ let wrapper;
+
+ Vue.use(VueApollo);
+
+ const mockAbuseReportId = mockAbuseReport.report.globalId;
+
+ const notesQueryHandler = jest.fn().mockResolvedValue(mockNotesByIdResponse);
+
+ const findSkeletonLoaders = () => wrapper.findAllComponents(SkeletonLoadingContainer);
+ const findAbuseReportDiscussions = () => wrapper.findAllComponents(AbuseReportDiscussion);
+
+ const createComponent = ({
+ queryHandler = notesQueryHandler,
+ abuseReportId = mockAbuseReportId,
+ } = {}) => {
+ wrapper = shallowMount(AbuseReportNotes, {
+ apolloProvider: createMockApollo([[abuseReportNotesQuery, queryHandler]]),
+ propsData: {
+ abuseReportId,
+ },
+ });
+ };
+
+ describe('when notes are loading', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('should show the skeleton loaders', () => {
+ expect(findSkeletonLoaders()).toHaveLength(5);
+ });
+ });
+
+ describe('when notes have been loaded', () => {
+ beforeEach(() => {
+ createComponent();
+ return waitForPromises();
+ });
+
+ it('should not render skeleton loader', () => {
+ expect(findSkeletonLoaders()).toHaveLength(0);
+ });
+
+ it('should call the abuse report notes query', () => {
+ expect(notesQueryHandler).toHaveBeenCalledWith({
+ id: mockAbuseReportId,
+ });
+ });
+
+ it('should show notes to the length of the response', () => {
+ expect(findAbuseReportDiscussions()).toHaveLength(2);
+
+ const discussions = mockNotesByIdResponse.data.abuseReport.discussions.nodes;
+
+ expect(findAbuseReportDiscussions().at(0).props()).toMatchObject({
+ abuseReportId: mockAbuseReportId,
+ discussion: discussions[0].notes.nodes,
+ });
+
+ expect(findAbuseReportDiscussions().at(1).props()).toMatchObject({
+ abuseReportId: mockAbuseReportId,
+ discussion: discussions[1].notes.nodes,
+ });
+ });
+ });
+
+ describe('When there is an error fetching the notes', () => {
+ beforeEach(() => {
+ createComponent({
+ queryHandler: jest.fn().mockRejectedValue(new Error()),
+ });
+
+ return waitForPromises();
+ });
+
+ it('should show an error when query fails', () => {
+ expect(createAlert).toHaveBeenCalledWith({
+ message: 'An error occurred while fetching comments, please try again.',
+ });
+ });
+ });
+});
diff --git a/spec/frontend/admin/abuse_report/components/labels_select_spec.js b/spec/frontend/admin/abuse_report/components/labels_select_spec.js
index a22dcc18e10..6eabaa33189 100644
--- a/spec/frontend/admin/abuse_report/components/labels_select_spec.js
+++ b/spec/frontend/admin/abuse_report/components/labels_select_spec.js
@@ -9,7 +9,7 @@ import LabelsSelect from '~/admin/abuse_report/components/labels_select.vue';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { stubComponent, RENDER_ALL_SLOTS_TEMPLATE } from 'helpers/stub_component';
-import labelsQuery from '~/admin/abuse_report/components/graphql/abuse_report_labels.query.graphql';
+import labelsQuery from '~/admin/abuse_report/graphql/abuse_report_labels.query.graphql';
import DropdownWidget from '~/vue_shared/components/dropdown/dropdown_widget/dropdown_widget.vue';
import DropdownValue from '~/sidebar/components/labels/labels_select_widget/dropdown_value.vue';
import DropdownHeader from '~/sidebar/components/labels/labels_select_widget/dropdown_header.vue';
diff --git a/spec/frontend/admin/abuse_report/components/notes/__snapshots__/abuse_report_note_body_spec.js.snap b/spec/frontend/admin/abuse_report/components/notes/__snapshots__/abuse_report_note_body_spec.js.snap
new file mode 100644
index 00000000000..5651a2a3eab
--- /dev/null
+++ b/spec/frontend/admin/abuse_report/components/notes/__snapshots__/abuse_report_note_body_spec.js.snap
@@ -0,0 +1,15 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Abuse Report Note Body should show the note body 1`] = `
+<div
+ class="md note-text"
+ data-testid="abuse-report-note-body"
+>
+ <p
+ data-sourcepos="1:1-1:9"
+ dir="auto"
+ >
+ Comment 1
+ </p>
+</div>
+`;
diff --git a/spec/frontend/admin/abuse_report/components/notes/abuse_report_discussion_spec.js b/spec/frontend/admin/abuse_report/components/notes/abuse_report_discussion_spec.js
new file mode 100644
index 00000000000..86f0939a938
--- /dev/null
+++ b/spec/frontend/admin/abuse_report/components/notes/abuse_report_discussion_spec.js
@@ -0,0 +1,79 @@
+import { shallowMount } from '@vue/test-utils';
+import { nextTick } from 'vue';
+import ToggleRepliesWidget from '~/notes/components/toggle_replies_widget.vue';
+import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
+import AbuseReportDiscussion from '~/admin/abuse_report/components/notes/abuse_report_discussion.vue';
+import AbuseReportNote from '~/admin/abuse_report/components/notes/abuse_report_note.vue';
+
+import {
+ mockAbuseReport,
+ mockDiscussionWithNoReplies,
+ mockDiscussionWithReplies,
+} from '../../mock_data';
+
+describe('Abuse Report Discussion', () => {
+ let wrapper;
+ const mockAbuseReportId = mockAbuseReport.report.globalId;
+
+ const findAbuseReportNote = () => wrapper.findComponent(AbuseReportNote);
+ const findAbuseReportNotes = () => wrapper.findAllComponents(AbuseReportNote);
+ const findTimelineEntryItem = () => wrapper.findComponent(TimelineEntryItem);
+ const findToggleRepliesWidget = () => wrapper.findComponent(ToggleRepliesWidget);
+
+ const createComponent = ({
+ discussion = mockDiscussionWithNoReplies,
+ abuseReportId = mockAbuseReportId,
+ } = {}) => {
+ wrapper = shallowMount(AbuseReportDiscussion, {
+ propsData: {
+ discussion,
+ abuseReportId,
+ },
+ });
+ };
+
+ describe('Default', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('should show the abuse report note', () => {
+ expect(findAbuseReportNote().exists()).toBe(true);
+
+ expect(findAbuseReportNote().props()).toMatchObject({
+ abuseReportId: mockAbuseReportId,
+ note: mockDiscussionWithNoReplies[0],
+ });
+ });
+
+ it('should not show timeline entry item component', () => {
+ expect(findTimelineEntryItem().exists()).toBe(false);
+ });
+
+ it('should not show the the toggle replies widget wrapper when no replies', () => {
+ expect(findToggleRepliesWidget().exists()).toBe(false);
+ });
+ });
+
+ describe('When the main comments has replies', () => {
+ beforeEach(() => {
+ createComponent({
+ discussion: mockDiscussionWithReplies,
+ });
+ });
+
+ it('should show the toggle replies widget', () => {
+ expect(findToggleRepliesWidget().exists()).toBe(true);
+ });
+
+ it('the number of replies should be equal to the response length', () => {
+ expect(findAbuseReportNotes()).toHaveLength(3);
+ });
+
+ it('should collapse when we click on toggle replies widget', async () => {
+ findToggleRepliesWidget().vm.$emit('toggle');
+ await nextTick();
+ expect(findAbuseReportNotes()).toHaveLength(1);
+ });
+ });
+});
diff --git a/spec/frontend/admin/abuse_report/components/notes/abuse_report_note_body_spec.js b/spec/frontend/admin/abuse_report/components/notes/abuse_report_note_body_spec.js
new file mode 100644
index 00000000000..25f675b4562
--- /dev/null
+++ b/spec/frontend/admin/abuse_report/components/notes/abuse_report_note_body_spec.js
@@ -0,0 +1,27 @@
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import AbuseReportNoteBody from '~/admin/abuse_report/components/notes/abuse_report_note_body.vue';
+import { mockDiscussionWithNoReplies } from '../../mock_data';
+
+describe('Abuse Report Note Body', () => {
+ let wrapper;
+ const mockNote = mockDiscussionWithNoReplies[0];
+
+ const findNoteBody = () => wrapper.findByTestId('abuse-report-note-body');
+
+ const createComponent = ({ note = mockNote } = {}) => {
+ wrapper = shallowMountExtended(AbuseReportNoteBody, {
+ propsData: {
+ note,
+ },
+ });
+ };
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('should show the note body', () => {
+ expect(findNoteBody().exists()).toBe(true);
+ expect(findNoteBody().html()).toMatchSnapshot();
+ });
+});
diff --git a/spec/frontend/admin/abuse_report/components/notes/abuse_report_note_spec.js b/spec/frontend/admin/abuse_report/components/notes/abuse_report_note_spec.js
new file mode 100644
index 00000000000..b6908853e46
--- /dev/null
+++ b/spec/frontend/admin/abuse_report/components/notes/abuse_report_note_spec.js
@@ -0,0 +1,80 @@
+import { shallowMount } from '@vue/test-utils';
+import { GlAvatarLink, GlAvatar } from '@gitlab/ui';
+import AbuseReportNote from '~/admin/abuse_report/components/notes/abuse_report_note.vue';
+import NoteHeader from '~/notes/components/note_header.vue';
+import NoteBody from '~/admin/abuse_report/components/notes/abuse_report_note_body.vue';
+
+import { mockAbuseReport, mockDiscussionWithNoReplies } from '../../mock_data';
+
+describe('Abuse Report Note', () => {
+ let wrapper;
+ const mockAbuseReportId = mockAbuseReport.report.globalId;
+ const mockNote = mockDiscussionWithNoReplies[0];
+
+ const findAvatar = () => wrapper.findComponent(GlAvatar);
+ const findAvatarLink = () => wrapper.findComponent(GlAvatarLink);
+
+ const findNoteHeader = () => wrapper.findComponent(NoteHeader);
+ const findNoteBody = () => wrapper.findComponent(NoteBody);
+
+ const createComponent = ({ note = mockNote, abuseReportId = mockAbuseReportId } = {}) => {
+ wrapper = shallowMount(AbuseReportNote, {
+ propsData: {
+ note,
+ abuseReportId,
+ },
+ });
+ };
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ describe('Author', () => {
+ const { author } = mockNote;
+
+ it('should show avatar', () => {
+ const avatar = findAvatar();
+
+ expect(avatar.exists()).toBe(true);
+ expect(avatar.props()).toMatchObject({
+ src: author.avatarUrl,
+ entityName: author.username,
+ alt: author.name,
+ });
+ });
+
+ it('should show avatar link with popover support', () => {
+ const avatarLink = findAvatarLink();
+
+ expect(avatarLink.exists()).toBe(true);
+ expect(avatarLink.classes()).toContain('js-user-link');
+ expect(avatarLink.attributes()).toMatchObject({
+ href: author.webUrl,
+ 'data-user-id': '1',
+ 'data-username': `${author.username}`,
+ });
+ });
+ });
+
+ describe('Header', () => {
+ it('should show note header', () => {
+ expect(findNoteHeader().exists()).toBe(true);
+ expect(findNoteHeader().props()).toMatchObject({
+ author: mockNote.author,
+ createdAt: mockNote.createdAt,
+ noteId: mockNote.id,
+ noteUrl: mockNote.url,
+ });
+ });
+ });
+
+ describe('Body', () => {
+ it('should show note body', () => {
+ expect(findNoteBody().exists()).toBe(true);
+ expect(findNoteBody().props()).toMatchObject({
+ note: mockNote,
+ });
+ });
+ });
+});
diff --git a/spec/frontend/admin/abuse_report/components/report_details_spec.js b/spec/frontend/admin/abuse_report/components/report_details_spec.js
index a5c43dcb82b..a7e732b43b0 100644
--- a/spec/frontend/admin/abuse_report/components/report_details_spec.js
+++ b/spec/frontend/admin/abuse_report/components/report_details_spec.js
@@ -5,7 +5,7 @@ import LabelsSelect from '~/admin/abuse_report/components/labels_select.vue';
import ReportDetails from '~/admin/abuse_report/components/report_details.vue';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
-import abuseReportQuery from '~/admin/abuse_report/components/graphql/abuse_report.query.graphql';
+import abuseReportQuery from '~/admin/abuse_report/graphql/abuse_report.query.graphql';
import { createAlert } from '~/alert';
import { mockAbuseReport, mockLabel1, mockReportQueryResponse } from '../mock_data';
diff --git a/spec/frontend/admin/abuse_report/mock_data.js b/spec/frontend/admin/abuse_report/mock_data.js
index ee61eabfa66..44c8cbdad7f 100644
--- a/spec/frontend/admin/abuse_report/mock_data.js
+++ b/spec/frontend/admin/abuse_report/mock_data.js
@@ -103,10 +103,14 @@ export const mockLabelsQueryResponse = {
export const mockReportQueryResponse = {
data: {
abuseReport: {
+ id: 'gid://gitlab/AbuseReport/1',
labels: {
nodes: [mockLabel1],
__typename: 'LabelConnection',
},
+ discussions: {
+ nodes: [],
+ },
__typename: 'AbuseReport',
},
},
@@ -128,3 +132,211 @@ export const mockCreateLabelResponse = {
},
},
};
+
+export const mockDiscussionWithNoReplies = [
+ {
+ id: 'gid://gitlab/Note/1',
+ body: 'Comment 1',
+ bodyHtml: '\u003cp data-sourcepos="1:1-1:9" dir="auto"\u003eComment 1\u003c/p\u003e',
+ createdAt: '2023-10-19T06:11:13Z',
+ lastEditedAt: '2023-10-20T02:46:50Z',
+ url: 'http://127.0.0.1:3000/admin/abuse_reports/1#note_1',
+ resolved: false,
+ author: {
+ id: 'gid://gitlab/User/1',
+ avatarUrl:
+ 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon',
+ name: 'Administrator',
+ username: 'root',
+ webUrl: 'http://127.0.0.1:3000/root',
+ __typename: 'UserCore',
+ },
+ lastEditedBy: null,
+ userPermissions: {
+ adminNote: true,
+ __typename: 'NotePermissions',
+ },
+ discussion: {
+ id: 'gid://gitlab/Discussion/055af96ab917175219aec8739c911277b18ea41d',
+ notes: {
+ nodes: [
+ {
+ id: 'gid://gitlab/Note/1',
+ __typename: 'Note',
+ },
+ ],
+ __typename: 'NoteConnection',
+ },
+ __typename: 'Discussion',
+ },
+ __typename: 'Note',
+ },
+];
+export const mockDiscussionWithReplies = [
+ {
+ id: 'gid://gitlab/DiscussionNote/2',
+ body: 'Comment 2',
+ bodyHtml: '\u003cp data-sourcepos="1:1-1:9" dir="auto"\u003eComment 2\u003c/p\u003e',
+ createdAt: '2023-10-20T07:47:21Z',
+ lastEditedAt: '2023-10-20T07:47:42Z',
+ url: 'http://127.0.0.1:3000/admin/abuse_reports/1#note_2',
+ resolved: false,
+ author: {
+ id: 'gid://gitlab/User/1',
+ avatarUrl:
+ 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon',
+ name: 'Administrator',
+ username: 'root',
+ webUrl: 'http://127.0.0.1:3000/root',
+ __typename: 'UserCore',
+ },
+ lastEditedBy: null,
+ userPermissions: {
+ adminNote: true,
+ __typename: 'NotePermissions',
+ },
+ discussion: {
+ id: 'gid://gitlab/Discussion/9c7228e06fb0339a3d1440fcda960acfd8baa43a',
+ notes: {
+ nodes: [
+ {
+ id: 'gid://gitlab/DiscussionNote/2',
+ __typename: 'Note',
+ },
+ {
+ id: 'gid://gitlab/DiscussionNote/3',
+ __typename: 'Note',
+ },
+ {
+ id: 'gid://gitlab/DiscussionNote/4',
+ __typename: 'Note',
+ },
+ ],
+ __typename: 'NoteConnection',
+ },
+ __typename: 'Discussion',
+ },
+ __typename: 'Note',
+ },
+ {
+ id: 'gid://gitlab/DiscussionNote/3',
+ body: 'Reply comment 1',
+ bodyHtml: '\u003cp data-sourcepos="1:1-1:15" dir="auto"\u003eReply comment 1\u003c/p\u003e',
+ createdAt: '2023-10-20T07:47:42Z',
+ lastEditedAt: '2023-10-20T07:47:42Z',
+ url: 'http://127.0.0.1:3000/admin/abuse_reports/1#note_3',
+ resolved: false,
+ author: {
+ id: 'gid://gitlab/User/1',
+ avatarUrl:
+ 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon',
+ name: 'Administrator',
+ username: 'root',
+ webUrl: 'http://127.0.0.1:3000/root',
+ __typename: 'UserCore',
+ },
+ lastEditedBy: null,
+ userPermissions: {
+ adminNote: true,
+ __typename: 'NotePermissions',
+ },
+ discussion: {
+ id: 'gid://gitlab/Discussion/9c7228e06fb0339a3d1440fcda960acfd8baa43a',
+ notes: {
+ nodes: [
+ {
+ id: 'gid://gitlab/DiscussionNote/2',
+ __typename: 'Note',
+ },
+ {
+ id: 'gid://gitlab/DiscussionNote/3',
+ __typename: 'Note',
+ },
+ {
+ id: 'gid://gitlab/DiscussionNote/4',
+ __typename: 'Note',
+ },
+ ],
+ __typename: 'NoteConnection',
+ },
+ __typename: 'Discussion',
+ },
+ __typename: 'Note',
+ },
+ {
+ id: 'gid://gitlab/DiscussionNote/4',
+ body: 'Reply comment 2',
+ bodyHtml: '\u003cp data-sourcepos="1:1-1:15" dir="auto"\u003eReply comment 2\u003c/p\u003e',
+ createdAt: '2023-10-20T08:26:51Z',
+ lastEditedAt: '2023-10-20T08:26:51Z',
+ url: 'http://127.0.0.1:3000/admin/abuse_reports/1#note_4',
+ resolved: false,
+ author: {
+ id: 'gid://gitlab/User/1',
+ avatarUrl:
+ 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon',
+ name: 'Administrator',
+ username: 'root',
+ webUrl: 'http://127.0.0.1:3000/root',
+ __typename: 'UserCore',
+ },
+ lastEditedBy: null,
+ userPermissions: {
+ adminNote: true,
+ __typename: 'NotePermissions',
+ },
+ discussion: {
+ id: 'gid://gitlab/Discussion/9c7228e06fb0339a3d1440fcda960acfd8baa43a',
+ notes: {
+ nodes: [
+ {
+ id: 'gid://gitlab/DiscussionNote/2',
+ __typename: 'Note',
+ },
+ {
+ id: 'gid://gitlab/DiscussionNote/3',
+ __typename: 'Note',
+ },
+ {
+ id: 'gid://gitlab/DiscussionNote/4',
+ __typename: 'Note',
+ },
+ ],
+ __typename: 'NoteConnection',
+ },
+ __typename: 'Discussion',
+ },
+ __typename: 'Note',
+ },
+];
+
+export const mockNotesByIdResponse = {
+ data: {
+ abuseReport: {
+ id: 'gid://gitlab/AbuseReport/1',
+ discussions: {
+ nodes: [
+ {
+ id: 'gid://gitlab/Discussion/055af96ab917175219aec8739c911277b18ea41d',
+ replyId:
+ 'gid://gitlab/IndividualNoteDiscussion/055af96ab917175219aec8739c911277b18ea41d',
+ notes: {
+ nodes: mockDiscussionWithNoReplies,
+ __typename: 'NoteConnection',
+ },
+ },
+ {
+ id: 'gid://gitlab/Discussion/9c7228e06fb0339a3d1440fcda960acfd8baa43a',
+ replyId: 'gid://gitlab/Discussion/9c7228e06fb0339a3d1440fcda960acfd8baa43a',
+ notes: {
+ nodes: mockDiscussionWithReplies,
+ __typename: 'NoteConnection',
+ },
+ },
+ ],
+ __typename: 'DiscussionConnection',
+ },
+ __typename: 'AbuseReport',
+ },
+ },
+};
diff --git a/spec/frontend/admin/users/components/app_spec.js b/spec/frontend/admin/users/components/app_spec.js
index d40089edc82..4b224947303 100644
--- a/spec/frontend/admin/users/components/app_spec.js
+++ b/spec/frontend/admin/users/components/app_spec.js
@@ -1,14 +1,42 @@
-import { shallowMount } from '@vue/test-utils';
-
+import { mount } from '@vue/test-utils';
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
import AdminUsersApp from '~/admin/users/components/app.vue';
-import AdminUsersTable from '~/admin/users/components/users_table.vue';
-import { users, paths } from '../mock_data';
+import UserActions from '~/admin/users/components/user_actions.vue';
+import getUsersGroupCountsQuery from '~/admin/users/graphql/queries/get_users_group_counts.query.graphql';
+import UsersTable from '~/vue_shared/components/users_table/users_table.vue';
+import { createAlert } from '~/alert';
+import { users, paths, createGroupCountResponse } from '../mock_data';
+
+Vue.use(VueApollo);
+
+jest.mock('~/alert');
describe('AdminUsersApp component', () => {
let wrapper;
+ const user = users[0];
+
+ const mockSuccessData = [{ id: user.id, groupCount: 5 }];
+ const mockParsedGroupCount = { 2177: 5 };
+ const mockError = new Error();
+
+ const createFetchGroupCount = (data) =>
+ jest.fn().mockResolvedValue(createGroupCountResponse(data));
+ const loadingResolver = jest.fn().mockResolvedValue(new Promise(() => {}));
+ const errorResolver = jest.fn().mockRejectedValueOnce(mockError);
+ const successfulResolver = createFetchGroupCount(mockSuccessData);
- const initComponent = (props = {}) => {
- wrapper = shallowMount(AdminUsersApp, {
+ function createMockApolloProvider(resolverMock) {
+ const requestHandlers = [[getUsersGroupCountsQuery, resolverMock]];
+
+ return createMockApollo(requestHandlers);
+ }
+
+ const initComponent = (props = {}, resolverMock = successfulResolver) => {
+ wrapper = mount(AdminUsersApp, {
+ apolloProvider: createMockApolloProvider(resolverMock),
propsData: {
users,
paths,
@@ -17,16 +45,47 @@ describe('AdminUsersApp component', () => {
});
};
- describe('when initialized', () => {
- beforeEach(() => {
+ const findUsersTable = () => wrapper.findComponent(UsersTable);
+ const findAllUserActions = () => wrapper.findAllComponents(UserActions);
+
+ describe.each`
+ description | mockResolver | loading | groupCounts | error
+ ${'when API call is loading'} | ${loadingResolver} | ${true} | ${{}} | ${false}
+ ${'when API returns successful with results'} | ${successfulResolver} | ${false} | ${mockParsedGroupCount} | ${false}
+ ${'when API returns error'} | ${errorResolver} | ${false} | ${{}} | ${true}
+ `('$description', ({ mockResolver, loading, groupCounts, error }) => {
+ beforeEach(async () => {
+ initComponent({}, mockResolver);
+ await waitForPromises();
+ });
+
+ it(`renders the UsersTable with group-counts-loading set to ${loading}`, () => {
+ expect(findUsersTable().props('groupCountsLoading')).toBe(loading);
+ });
+
+ it('renders the UsersTable with the correct group-counts data', () => {
+ expect(findUsersTable().props('groupCounts')).toStrictEqual(groupCounts);
+ });
+
+ it(`does ${error ? '' : 'not '}render an error message`, () => {
+ return error
+ ? expect(createAlert).toHaveBeenCalledWith({
+ message: 'Could not load user group counts. Please refresh the page to try again.',
+ error: mockError,
+ captureError: true,
+ })
+ : expect(createAlert).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('UserActions', () => {
+ beforeEach(async () => {
initComponent();
+ await waitForPromises();
});
- it('renders the admin users table with props', () => {
- expect(wrapper.findComponent(AdminUsersTable).props()).toEqual({
- users,
- paths,
- });
+ it('renders a UserActions component for each user', () => {
+ expect(findAllUserActions().wrappers.map((w) => w.props('user'))).toStrictEqual(users);
});
});
});
diff --git a/spec/frontend/admin/users/components/users_table_spec.js b/spec/frontend/admin/users/components/users_table_spec.js
deleted file mode 100644
index 6f658fd2e59..00000000000
--- a/spec/frontend/admin/users/components/users_table_spec.js
+++ /dev/null
@@ -1,141 +0,0 @@
-import { GlTable, GlSkeletonLoader } from '@gitlab/ui';
-import Vue from 'vue';
-import VueApollo from 'vue-apollo';
-
-import createMockApollo from 'helpers/mock_apollo_helper';
-import waitForPromises from 'helpers/wait_for_promises';
-import { mountExtended } from 'helpers/vue_test_utils_helper';
-
-import AdminUserActions from '~/admin/users/components/user_actions.vue';
-import AdminUserAvatar from '~/admin/users/components/user_avatar.vue';
-import AdminUsersTable from '~/admin/users/components/users_table.vue';
-import getUsersGroupCountsQuery from '~/admin/users/graphql/queries/get_users_group_counts.query.graphql';
-import { createAlert } from '~/alert';
-import AdminUserDate from '~/vue_shared/components/user_date.vue';
-
-import { users, paths, createGroupCountResponse } from '../mock_data';
-
-jest.mock('~/alert');
-
-Vue.use(VueApollo);
-
-describe('AdminUsersTable component', () => {
- let wrapper;
- const user = users[0];
-
- const createFetchGroupCount = (data) =>
- jest.fn().mockResolvedValue(createGroupCountResponse(data));
- const fetchGroupCountsLoading = jest.fn().mockResolvedValue(new Promise(() => {}));
- const fetchGroupCountsError = jest.fn().mockRejectedValue(new Error('Network error'));
- const fetchGroupCountsResponse = createFetchGroupCount([{ id: user.id, groupCount: 5 }]);
-
- const findUserGroupCount = (id) => wrapper.findByTestId(`user-group-count-${id}`);
- const findUserGroupCountLoader = (id) => findUserGroupCount(id).findComponent(GlSkeletonLoader);
- const getCellByLabel = (trIdx, label) => {
- return wrapper
- .findComponent(GlTable)
- .find('tbody')
- .findAll('tr')
- .at(trIdx)
- .find(`[data-label="${label}"][role="cell"]`);
- };
-
- function createMockApolloProvider(resolverMock) {
- const requestHandlers = [[getUsersGroupCountsQuery, resolverMock]];
-
- return createMockApollo(requestHandlers);
- }
-
- const initComponent = (props = {}, resolverMock = fetchGroupCountsResponse) => {
- wrapper = mountExtended(AdminUsersTable, {
- apolloProvider: createMockApolloProvider(resolverMock),
- propsData: {
- users,
- paths,
- ...props,
- },
- });
- };
-
- describe('when there are users', () => {
- beforeEach(() => {
- initComponent();
- });
-
- it('renders the projects count', () => {
- expect(getCellByLabel(0, 'Projects').text()).toContain(`${user.projectsCount}`);
- });
-
- it('renders the user actions', () => {
- expect(wrapper.findComponent(AdminUserActions).exists()).toBe(true);
- });
-
- it.each`
- component | label
- ${AdminUserAvatar} | ${'Name'}
- ${AdminUserDate} | ${'Created on'}
- ${AdminUserDate} | ${'Last activity'}
- `('renders the component for column $label', ({ component, label }) => {
- expect(getCellByLabel(0, label).findComponent(component).exists()).toBe(true);
- });
- });
-
- describe('when users is an empty array', () => {
- beforeEach(() => {
- initComponent({ users: [] });
- });
-
- it('renders a "No users found" message', () => {
- expect(wrapper.text()).toContain('No users found');
- });
- });
-
- describe('group counts', () => {
- describe('when fetching the data', () => {
- beforeEach(() => {
- initComponent({}, fetchGroupCountsLoading);
- });
-
- it('renders a loader for each user', () => {
- expect(findUserGroupCountLoader(user.id).exists()).toBe(true);
- });
- });
-
- describe('when the data has been fetched', () => {
- beforeEach(async () => {
- initComponent();
- await waitForPromises();
- });
-
- it("renders the user's group count", () => {
- expect(findUserGroupCount(user.id).text()).toBe('5');
- });
-
- describe("and a user's group count is null", () => {
- beforeEach(async () => {
- initComponent({}, createFetchGroupCount([{ id: user.id, groupCount: null }]));
- await waitForPromises();
- });
-
- it("renders the user's group count as 0", () => {
- expect(findUserGroupCount(user.id).text()).toBe('0');
- });
- });
- });
-
- describe('when there is an error while fetching the data', () => {
- beforeEach(async () => {
- initComponent({}, fetchGroupCountsError);
- await waitForPromises();
- });
-
- it('creates an alert message and captures the error', () => {
- expect(createAlert).toHaveBeenCalledWith({
- message: 'Could not load user group counts. Please refresh the page to try again.',
- captureError: true,
- error: expect.any(Error),
- });
- });
- });
- });
-});
diff --git a/spec/frontend/admin/users/constants.js b/spec/frontend/admin/users/constants.js
index d341eb03b1b..39e8e51f43c 100644
--- a/spec/frontend/admin/users/constants.js
+++ b/spec/frontend/admin/users/constants.js
@@ -9,6 +9,8 @@ const REJECT = 'reject';
const APPROVE = 'approve';
const BAN = 'ban';
const UNBAN = 'unban';
+const TRUST = 'trust';
+const UNTRUST = 'untrust';
export const EDIT = 'edit';
@@ -24,6 +26,8 @@ export const CONFIRMATION_ACTIONS = [
UNBAN,
APPROVE,
REJECT,
+ TRUST,
+ UNTRUST,
];
export const DELETE_ACTIONS = [DELETE, DELETE_WITH_CONTRIBUTIONS];
diff --git a/spec/frontend/alert_spec.js b/spec/frontend/alert_spec.js
index de3093c6c19..71c7dbe0cfd 100644
--- a/spec/frontend/alert_spec.js
+++ b/spec/frontend/alert_spec.js
@@ -1,8 +1,8 @@
-import * as Sentry from '@sentry/browser';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import { createAlert, VARIANT_WARNING } from '~/alert';
-jest.mock('@sentry/browser');
+jest.mock('~/sentry/sentry_browser_wrapper');
describe('Flash', () => {
const findTextContent = (containerSelector = '.flash-container') =>
diff --git a/spec/frontend/analytics/cycle_analytics/components/filter_bar_spec.js b/spec/frontend/analytics/cycle_analytics/components/filter_bar_spec.js
index 387d0b453ee..3b2606d494a 100644
--- a/spec/frontend/analytics/cycle_analytics/components/filter_bar_spec.js
+++ b/spec/frontend/analytics/cycle_analytics/components/filter_bar_spec.js
@@ -216,7 +216,7 @@ describe('Filter bar', () => {
urlUtils.mergeUrlParams = jest.fn();
mock = new MockAdapter(axios);
- wrapper = createComponent(storeConfig);
+ wrapper = createComponent(storeConfig());
wrapper.vm.$store.dispatch('filters/setFilters', {
...initialFilterBarState,
diff --git a/spec/frontend/analytics/cycle_analytics/mock_data.js b/spec/frontend/analytics/cycle_analytics/mock_data.js
index 7ad95cab9ad..e0b6f4aa8c4 100644
--- a/spec/frontend/analytics/cycle_analytics/mock_data.js
+++ b/spec/frontend/analytics/cycle_analytics/mock_data.js
@@ -11,7 +11,7 @@ import {
DEFAULT_VALUE_STREAM,
PAGINATION_TYPE,
PAGINATION_SORT_DIRECTION_DESC,
- PAGINATION_SORT_FIELD_END_EVENT,
+ PAGINATION_SORT_FIELD_DURATION,
} from '~/analytics/cycle_analytics/constants';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { getDateInPast } from '~/lib/utils/datetime_utility';
@@ -245,7 +245,7 @@ export const valueStreamStages = rawValueStreamStages.map((s) =>
export const initialPaginationQuery = {
page: 15,
- sort: PAGINATION_SORT_FIELD_END_EVENT,
+ sort: PAGINATION_SORT_FIELD_DURATION,
direction: PAGINATION_SORT_DIRECTION_DESC,
};
@@ -257,7 +257,7 @@ export const initialPaginationState = {
export const basePaginationResult = {
pagination: PAGINATION_TYPE,
- sort: PAGINATION_SORT_FIELD_END_EVENT,
+ sort: PAGINATION_SORT_FIELD_DURATION,
direction: PAGINATION_SORT_DIRECTION_DESC,
page: null,
};
diff --git a/spec/frontend/analytics/cycle_analytics/store/mutations_spec.js b/spec/frontend/analytics/cycle_analytics/store/mutations_spec.js
index 25fed2b1714..a37f37aaaf4 100644
--- a/spec/frontend/analytics/cycle_analytics/store/mutations_spec.js
+++ b/spec/frontend/analytics/cycle_analytics/store/mutations_spec.js
@@ -2,7 +2,7 @@ import { useFakeDate } from 'helpers/fake_date';
import * as types from '~/analytics/cycle_analytics/store/mutation_types';
import mutations from '~/analytics/cycle_analytics/store/mutations';
import {
- PAGINATION_SORT_FIELD_END_EVENT,
+ PAGINATION_SORT_FIELD_DURATION,
PAGINATION_SORT_DIRECTION_DESC,
} from '~/analytics/cycle_analytics/constants';
import {
@@ -99,7 +99,7 @@ describe('Project Value Stream Analytics mutations', () => {
${types.SET_LOADING} | ${true} | ${'isLoading'} | ${true}
${types.SET_LOADING} | ${false} | ${'isLoading'} | ${false}
${types.SET_SELECTED_VALUE_STREAM} | ${selectedValueStream} | ${'selectedValueStream'} | ${selectedValueStream}
- ${types.SET_PAGINATION} | ${pagination} | ${'pagination'} | ${{ ...pagination, sort: PAGINATION_SORT_FIELD_END_EVENT, direction: PAGINATION_SORT_DIRECTION_DESC }}
+ ${types.SET_PAGINATION} | ${pagination} | ${'pagination'} | ${{ ...pagination, sort: PAGINATION_SORT_FIELD_DURATION, direction: PAGINATION_SORT_DIRECTION_DESC }}
${types.SET_PAGINATION} | ${{ ...pagination, sort: 'duration', direction: 'asc' }} | ${'pagination'} | ${{ ...pagination, sort: 'duration', direction: 'asc' }}
${types.SET_SELECTED_STAGE} | ${selectedStage} | ${'selectedStage'} | ${selectedStage}
${types.RECEIVE_VALUE_STREAMS_SUCCESS} | ${[selectedValueStream]} | ${'valueStreams'} | ${[selectedValueStream]}
diff --git a/spec/frontend/analytics/product_analytics/components/activity_chart_spec.js b/spec/frontend/analytics/product_analytics/components/activity_chart_spec.js
deleted file mode 100644
index 4f8126aaacf..00000000000
--- a/spec/frontend/analytics/product_analytics/components/activity_chart_spec.js
+++ /dev/null
@@ -1,34 +0,0 @@
-import { GlColumnChart } from '@gitlab/ui/dist/charts';
-import { shallowMount } from '@vue/test-utils';
-import ActivityChart from '~/analytics/product_analytics/components/activity_chart.vue';
-
-describe('Activity Chart Bundle', () => {
- let wrapper;
- function mountComponent({ provide }) {
- wrapper = shallowMount(ActivityChart, {
- provide: {
- formattedData: {},
- ...provide,
- },
- });
- }
-
- const findChart = () => wrapper.findComponent(GlColumnChart);
- const findNoData = () => wrapper.find('[data-testid="noActivityChartData"]');
-
- describe('Activity Chart', () => {
- it('renders an warning message with no data', () => {
- mountComponent({ provide: { formattedData: {} } });
- expect(findNoData().exists()).toBe(true);
- });
-
- it('renders a chart with data', () => {
- mountComponent({
- provide: { formattedData: { keys: ['key1', 'key2'], values: [5038, 2241] } },
- });
-
- expect(findNoData().exists()).toBe(false);
- expect(findChart().exists()).toBe(true);
- });
- });
-});
diff --git a/spec/frontend/analytics/shared/components/metric_tile_spec.js b/spec/frontend/analytics/shared/components/metric_tile_spec.js
index 9da5ed0fb07..262357a35e4 100644
--- a/spec/frontend/analytics/shared/components/metric_tile_spec.js
+++ b/spec/frontend/analytics/shared/components/metric_tile_spec.js
@@ -32,8 +32,7 @@ describe('MetricTile', () => {
};
wrapper = createComponent({ metric });
- const singleStat = findSingleStat();
- singleStat.vm.$emit('click');
+ findSingleStat().vm.$emit('click');
expect(redirectTo).toHaveBeenCalledWith('foo/bar'); // eslint-disable-line import/no-deprecated
});
@@ -41,27 +40,31 @@ describe('MetricTile', () => {
const metric = { identifier: 'deploys', value: '10', label: 'Deploys' };
wrapper = createComponent({ metric });
- const singleStat = findSingleStat();
- singleStat.vm.$emit('click');
+ findSingleStat().vm.$emit('click');
expect(redirectTo).not.toHaveBeenCalled(); // eslint-disable-line import/no-deprecated
});
});
- describe('decimal places', () => {
+ describe('number formatting', () => {
it(`will render 0 decimal places for an integer value`, () => {
const metric = { identifier: 'deploys', value: '10', label: 'Deploys' };
wrapper = createComponent({ metric });
- const singleStat = findSingleStat();
- expect(singleStat.props('animationDecimalPlaces')).toBe(0);
+ expect(findSingleStat().props('animationDecimalPlaces')).toBe(0);
});
it(`will render 1 decimal place for a float value`, () => {
const metric = { identifier: 'deploys', value: '10.5', label: 'Deploys' };
wrapper = createComponent({ metric });
- const singleStat = findSingleStat();
- expect(singleStat.props('animationDecimalPlaces')).toBe(1);
+ expect(findSingleStat().props('animationDecimalPlaces')).toBe(1);
+ });
+
+ it('will render using delimiters', () => {
+ const metric = { identifier: 'deploys', value: '10000', label: 'Deploys' };
+ wrapper = createComponent({ metric });
+
+ expect(findSingleStat().props('useDelimiters')).toBe(true);
});
});
diff --git a/spec/frontend/authentication/two_factor_auth/components/recovery_codes_spec.js b/spec/frontend/authentication/two_factor_auth/components/recovery_codes_spec.js
index aef06a74fdd..086a4bc1ec0 100644
--- a/spec/frontend/authentication/two_factor_auth/components/recovery_codes_spec.js
+++ b/spec/frontend/authentication/two_factor_auth/components/recovery_codes_spec.js
@@ -21,7 +21,7 @@ describe('RecoveryCodes', () => {
propsData: {
codes,
profileAccountPath,
- ...(options?.propsData || {}),
+ ...options?.propsData,
},
...options,
}),
diff --git a/spec/frontend/awards_handler_spec.js b/spec/frontend/awards_handler_spec.js
index c2b7906d0d6..c2a878e661d 100644
--- a/spec/frontend/awards_handler_spec.js
+++ b/spec/frontend/awards_handler_spec.js
@@ -13,62 +13,71 @@ let awardsHandler = null;
describe('AwardsHandler', () => {
useFakeRequestAnimationFrame();
- const emojiData = {
- '8ball': {
+ const emojiData = [
+ {
+ n: '8ball',
c: 'activity',
e: '🎱',
d: 'billiards',
u: '6.0',
},
- grinning: {
+ {
+ n: 'grinning',
c: 'people',
e: '😀',
d: 'grinning face',
u: '6.1',
},
- angel: {
+ {
+ n: 'angel',
c: 'people',
e: '👼',
d: 'baby angel',
u: '6.0',
},
- anger: {
+ {
+ n: 'anger',
c: 'symbols',
e: '💢',
d: 'anger symbol',
u: '6.0',
},
- alien: {
+ {
+ n: 'alien',
c: 'people',
e: '👽',
d: 'extraterrestrial alien',
u: '6.0',
},
- sunglasses: {
+ {
+ n: 'sunglasses',
c: 'people',
e: '😎',
d: 'smiling face with sunglasses',
u: '6.0',
},
- grey_question: {
+ {
+ n: 'grey_question',
c: 'symbols',
e: '❔',
d: 'white question mark ornament',
u: '6.0',
},
- thumbsup: {
+ {
+ n: 'thumbsup',
c: 'people',
e: '👍',
d: 'thumbs up sign',
u: '6.0',
},
- thumbsdown: {
+ {
+ n: 'thumbsdown',
c: 'people',
e: '👎',
d: 'thumbs down sign',
u: '6.0',
},
- };
+ ];
const openAndWaitForEmojiMenu = (sel = '.js-add-award') => {
$(sel).eq(0).click();
diff --git a/spec/frontend/batch_comments/components/submit_dropdown_spec.js b/spec/frontend/batch_comments/components/submit_dropdown_spec.js
index 19be3fb7d31..9e0b13c7e6e 100644
--- a/spec/frontend/batch_comments/components/submit_dropdown_spec.js
+++ b/spec/frontend/batch_comments/components/submit_dropdown_spec.js
@@ -1,22 +1,51 @@
-import { GlDropdown } from '@gitlab/ui';
+import { GlDisclosureDropdown } from '@gitlab/ui';
import Vue from 'vue';
+import VueApollo from 'vue-apollo';
// eslint-disable-next-line no-restricted-imports
import Vuex from 'vuex';
+import createMockApollo from 'helpers/mock_apollo_helper';
import { mountExtended } from 'helpers/vue_test_utils_helper';
+import waitForPromises from 'helpers/wait_for_promises';
import SubmitDropdown from '~/batch_comments/components/submit_dropdown.vue';
import { mockTracking } from 'helpers/tracking_helper';
+import userCanApproveQuery from '~/batch_comments/queries/can_approve.query.graphql';
jest.mock('~/autosave');
+Vue.use(VueApollo);
Vue.use(Vuex);
let wrapper;
let publishReview;
let trackingSpy;
-function factory({ canApprove = true, shouldAnimateReviewButton = false } = {}) {
+function factory({
+ canApprove = true,
+ shouldAnimateReviewButton = false,
+ mrRequestChanges = false,
+} = {}) {
publishReview = jest.fn();
trackingSpy = mockTracking(undefined, null, jest.spyOn);
+ const requestHandlers = [
+ [
+ userCanApproveQuery,
+ () =>
+ Promise.resolve({
+ data: {
+ project: {
+ id: 1,
+ mergeRequest: {
+ id: 1,
+ userPermissions: {
+ canApprove,
+ },
+ },
+ },
+ },
+ }),
+ ],
+ ];
+ const apolloProvider = createMockApollo(requestHandlers);
const store = new Vuex.Store({
getters: {
@@ -27,12 +56,17 @@ function factory({ canApprove = true, shouldAnimateReviewButton = false } = {})
getNoteableData: () => ({
id: 1,
preview_note_path: '/preview',
- current_user: { can_approve: canApprove },
}),
noteableType: () => 'merge_request',
getCurrentUserLastNote: () => ({ id: 1 }),
},
modules: {
+ diffs: {
+ namespaced: true,
+ state: {
+ projectPath: 'gitlab-org/gitlab',
+ },
+ },
batchComments: {
namespaced: true,
state: { shouldAnimateReviewButton },
@@ -44,13 +78,17 @@ function factory({ canApprove = true, shouldAnimateReviewButton = false } = {})
});
wrapper = mountExtended(SubmitDropdown, {
store,
+ apolloProvider,
+ provide: {
+ glFeatures: { mrRequestChanges },
+ },
});
}
const findCommentTextarea = () => wrapper.findByTestId('comment-textarea');
const findSubmitButton = () => wrapper.findByTestId('submit-review-button');
const findForm = () => wrapper.findByTestId('submit-gl-form');
-const findSubmitDropdown = () => wrapper.findComponent(GlDropdown);
+const findSubmitDropdown = () => wrapper.findComponent(GlDisclosureDropdown);
describe('Batch comments submit dropdown', () => {
afterEach(() => {
@@ -70,6 +108,7 @@ describe('Batch comments submit dropdown', () => {
note: 'Hello world',
approve: false,
approval_password: '',
+ reviewer_state: 'reviewed',
});
});
@@ -113,11 +152,18 @@ describe('Batch comments submit dropdown', () => {
canApprove | exists | existsText
${true} | ${true} | ${'shows'}
${false} | ${false} | ${'hides'}
- `('$existsText approve checkbox if can_approve is $canApprove', ({ canApprove, exists }) => {
- factory({ canApprove });
+ `(
+ '$existsText approve checkbox if can_approve is $canApprove',
+ async ({ canApprove, exists }) => {
+ factory({ canApprove });
- expect(wrapper.findByTestId('approve_merge_request').exists()).toBe(exists);
- });
+ wrapper.findComponent(GlDisclosureDropdown).vm.$emit('shown');
+
+ await waitForPromises();
+
+ expect(wrapper.findByTestId('approve_merge_request').exists()).toBe(exists);
+ },
+ );
it.each`
shouldAnimateReviewButton | animationClassApplied | classText
@@ -133,4 +179,52 @@ describe('Batch comments submit dropdown', () => {
);
},
);
+
+ describe('when mrRequestChanges feature flag is enabled', () => {
+ it('renders a radio group with review state options', async () => {
+ factory({ mrRequestChanges: true });
+
+ await waitForPromises();
+
+ expect(wrapper.findAll('.gl-form-radio').length).toBe(3);
+ });
+
+ it('renders disabled approve radio button when user can not approve', async () => {
+ factory({ mrRequestChanges: true, canApprove: false });
+
+ wrapper.findComponent(GlDisclosureDropdown).vm.$emit('shown');
+
+ await waitForPromises();
+
+ expect(wrapper.find('.custom-control-input[value="approved"]').attributes('disabled')).toBe(
+ 'disabled',
+ );
+ });
+
+ it.each`
+ value
+ ${'approved'}
+ ${'reviewed'}
+ ${'requested_changes'}
+ `('sends $value review state to api when submitting', async ({ value }) => {
+ factory({ mrRequestChanges: true });
+
+ wrapper.findComponent(GlDisclosureDropdown).vm.$emit('shown');
+
+ await waitForPromises();
+
+ await wrapper.find(`.custom-control-input[value="${value}"]`).trigger('change');
+
+ findForm().vm.$emit('submit', { preventDefault: jest.fn() });
+
+ expect(publishReview).toHaveBeenCalledWith(expect.anything(), {
+ noteable_type: 'merge_request',
+ noteable_id: 1,
+ note: 'Hello world',
+ approve: false,
+ approval_password: '',
+ reviewer_state: value,
+ });
+ });
+ });
});
diff --git a/spec/frontend/batch_comments/stores/modules/batch_comments/actions_spec.js b/spec/frontend/batch_comments/stores/modules/batch_comments/actions_spec.js
index 824b2a296c6..3f8083aa37d 100644
--- a/spec/frontend/batch_comments/stores/modules/batch_comments/actions_spec.js
+++ b/spec/frontend/batch_comments/stores/modules/batch_comments/actions_spec.js
@@ -191,8 +191,6 @@ describe('Batch comments store actions', () => {
return actions.publishReview({ dispatch, commit, getters, rootGetters }).then(() => {
expect(commit.mock.calls[0]).toEqual(['REQUEST_PUBLISH_REVIEW']);
expect(commit.mock.calls[1]).toEqual(['RECEIVE_PUBLISH_REVIEW_SUCCESS']);
-
- expect(dispatch.mock.calls[0]).toEqual(['updateDiscussionsAfterPublish']);
});
});
diff --git a/spec/frontend/behaviors/gl_emoji_spec.js b/spec/frontend/behaviors/gl_emoji_spec.js
index c7f4fce0e4c..ef40179c23b 100644
--- a/spec/frontend/behaviors/gl_emoji_spec.js
+++ b/spec/frontend/behaviors/gl_emoji_spec.js
@@ -15,20 +15,22 @@ jest.mock('~/lib/graphql', () => {
});
describe('gl_emoji', () => {
- const emojiData = {
- grey_question: {
+ const emojiData = [
+ {
+ n: 'grey_question',
c: 'symbols',
e: '❔',
d: 'white question mark ornament',
u: '6.0',
},
- bomb: {
+ {
+ n: 'bomb',
c: 'objects',
e: '💣',
d: 'bomb',
u: '6.0',
},
- };
+ ];
beforeAll(() => {
jest.spyOn(EmojiUnicodeSupport, 'default').mockReturnValue(true);
@@ -119,7 +121,7 @@ describe('gl_emoji', () => {
await waitForPromises();
expect(glEmojiElement.outerHTML).toBe(
- '<gl-emoji data-name="&quot;x=&quot;y&quot; onload=&quot;alert(document.location.href)&quot;" data-unicode-version="x"><img class="emoji" title=":&quot;x=&quot;y&quot; onload=&quot;alert(document.location.href)&quot;:" alt=":&quot;x=&quot;y&quot; onload=&quot;alert(document.location.href)&quot;:" src="/-/emojis/2/grey_question.png" align="absmiddle"></gl-emoji>',
+ '<gl-emoji data-name="&quot;x=&quot;y&quot; onload=&quot;alert(document.location.href)&quot;" data-unicode-version="x"><img class="emoji" title=":&quot;x=&quot;y&quot; onload=&quot;alert(document.location.href)&quot;:" alt=":&quot;x=&quot;y&quot; onload=&quot;alert(document.location.href)&quot;:" src="/-/emojis/3/grey_question.png" align="absmiddle"></gl-emoji>',
);
});
diff --git a/spec/frontend/behaviors/load_startup_css_spec.js b/spec/frontend/behaviors/load_startup_css_spec.js
deleted file mode 100644
index e9e4c06732f..00000000000
--- a/spec/frontend/behaviors/load_startup_css_spec.js
+++ /dev/null
@@ -1,48 +0,0 @@
-import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
-import { loadStartupCSS } from '~/behaviors/load_startup_css';
-
-describe('behaviors/load_startup_css', () => {
- let loadListener;
-
- const setupListeners = () => {
- document
- .querySelectorAll('link')
- .forEach((x) => x.addEventListener('load', () => loadListener(x)));
- };
-
- beforeEach(() => {
- loadListener = jest.fn();
-
- setHTMLFixture(`
- <meta charset="utf-8" />
- <link media="print" src="./lorem-print.css" />
- <link media="print" src="./ipsum-print.css" />
- <link media="all" src="./dolar-all.css" />
- `);
-
- setupListeners();
-
- loadStartupCSS();
- });
-
- afterEach(() => {
- resetHTMLFixture();
- });
-
- it('does nothing at first', () => {
- expect(loadListener).not.toHaveBeenCalled();
- });
-
- describe('on window load', () => {
- beforeEach(() => {
- window.dispatchEvent(new Event('load'));
- });
-
- it('dispatches load to the print links', () => {
- expect(loadListener.mock.calls.map(([el]) => el.getAttribute('src'))).toEqual([
- './lorem-print.css',
- './ipsum-print.css',
- ]);
- });
- });
-});
diff --git a/spec/frontend/blob/components/blob_header_spec.js b/spec/frontend/blob/components/blob_header_spec.js
index 922d6a0211b..e7b2ee74940 100644
--- a/spec/frontend/blob/components/blob_header_spec.js
+++ b/spec/frontend/blob/components/blob_header_spec.js
@@ -116,13 +116,22 @@ describe('Blob Header Default Actions', () => {
});
});
+ it.each([[{ showBlameToggle: true }], [{ showBlameToggle: false }]])(
+ 'passes the `showBlameToggle` prop to the viewer switcher',
+ (propsData) => {
+ createComponent({ propsData });
+
+ expect(findViewSwitcher().props('showBlameToggle')).toBe(propsData.showBlameToggle);
+ },
+ );
+
it('does not render viewer switcher if the blob has only the simple viewer', () => {
createComponent({
blobProps: {
richViewer: null,
},
});
- expect(findViewSwitcher().exists()).toBe(false);
+ expect(findViewSwitcher().props('showViewerToggles')).toBe(false);
});
it('does not render viewer switcher if a corresponding prop is passed', () => {
diff --git a/spec/frontend/blob/components/blob_header_viewer_switcher_spec.js b/spec/frontend/blob/components/blob_header_viewer_switcher_spec.js
index 2ef87f6664b..25d9642acb0 100644
--- a/spec/frontend/blob/components/blob_header_viewer_switcher_spec.js
+++ b/spec/frontend/blob/components/blob_header_viewer_switcher_spec.js
@@ -1,6 +1,6 @@
import { GlButtonGroup, GlButton } from '@gitlab/ui';
-import { mount } from '@vue/test-utils';
import { nextTick } from 'vue';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
import BlobHeaderViewerSwitcher from '~/blob/components/blob_header_viewer_switcher.vue';
import {
RICH_BLOB_VIEWER,
@@ -12,14 +12,15 @@ import {
describe('Blob Header Viewer Switcher', () => {
let wrapper;
- function createComponent(propsData = {}) {
- wrapper = mount(BlobHeaderViewerSwitcher, {
+ function createComponent(propsData = { showViewerToggles: true }) {
+ wrapper = mountExtended(BlobHeaderViewerSwitcher, {
propsData,
});
}
const findSimpleViewerButton = () => wrapper.findComponent('[data-viewer="simple"]');
const findRichViewerButton = () => wrapper.findComponent('[data-viewer="rich"]');
+ const findBlameButton = () => wrapper.findByText('Blame');
describe('intiialization', () => {
it('is initialized with simple viewer as active', () => {
@@ -74,7 +75,7 @@ describe('Blob Header Viewer Switcher', () => {
});
it('emits an event when a Simple Viewer button is clicked', async () => {
- createComponent({ value: RICH_BLOB_VIEWER });
+ createComponent({ value: RICH_BLOB_VIEWER, showViewerToggles: true });
findSimpleViewerButton().vm.$emit('click');
await nextTick();
@@ -82,4 +83,28 @@ describe('Blob Header Viewer Switcher', () => {
expect(wrapper.emitted('input')).toEqual([[SIMPLE_BLOB_VIEWER]]);
});
});
+
+ it('does not render simple and rich viewer buttons if `showViewerToggles` is `false`', async () => {
+ createComponent({ showViewerToggles: false });
+ await nextTick();
+
+ expect(findSimpleViewerButton().exists()).toBe(false);
+ expect(findRichViewerButton().exists()).toBe(false);
+ });
+
+ it('does not render a Blame button if `showBlameToggle` is `false`', async () => {
+ createComponent({ showBlameToggle: false });
+ await nextTick();
+
+ expect(findBlameButton().exists()).toBe(false);
+ });
+
+ it('emits an event when the Blame button is clicked', async () => {
+ createComponent({ showBlameToggle: true });
+
+ findBlameButton().trigger('click');
+ await nextTick();
+
+ expect(wrapper.emitted('blame')).toHaveLength(1);
+ });
});
diff --git a/spec/frontend/boards/board_card_inner_spec.js b/spec/frontend/boards/board_card_inner_spec.js
index 8314cbda7a1..c70e461da83 100644
--- a/spec/frontend/boards/board_card_inner_spec.js
+++ b/spec/frontend/boards/board_card_inner_spec.js
@@ -1,4 +1,4 @@
-import { GlLabel, GlLoadingIcon, GlTooltip } from '@gitlab/ui';
+import { GlLabel, GlLoadingIcon } from '@gitlab/ui';
import { range } from 'lodash';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
@@ -47,13 +47,6 @@ describe('Board card component', () => {
const findIssuableBlockedIcon = () => wrapper.findComponent(IssuableBlockedIcon);
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
- const findEpicCountablesTotalTooltip = () => wrapper.findComponent(GlTooltip);
- const findEpicCountables = () => wrapper.findByTestId('epic-countables');
- const findEpicCountablesBadgeIssues = () => wrapper.findByTestId('epic-countables-counts-issues');
- const findEpicCountablesBadgeWeight = () => wrapper.findByTestId('epic-countables-weight-issues');
- const findEpicBadgeProgress = () => wrapper.findByTestId('epic-progress');
- const findEpicCountablesTotalWeight = () => wrapper.findByTestId('epic-countables-total-weight');
- const findEpicProgressTooltip = () => wrapper.findByTestId('epic-progress-tooltip-content');
const findHiddenIssueIcon = () => wrapper.findByTestId('hidden-icon');
const findWorkItemIcon = () => wrapper.findComponent(WorkItemTypeIcon);
@@ -70,7 +63,7 @@ describe('Board card component', () => {
const mockApollo = createMockApollo();
- const createWrapper = ({ props = {}, isEpicBoard = false, isGroupBoard = true } = {}) => {
+ const createWrapper = ({ props = {}, isGroupBoard = true } = {}) => {
mockApollo.clients.defaultClient.cache.writeQuery({
query: isShowingLabelsQuery,
data: {
@@ -97,8 +90,8 @@ describe('Board card component', () => {
provide: {
rootPath: '/',
scopedLabelsAvailable: false,
- isEpicBoard,
- allowSubEpics: isEpicBoard,
+ isEpicBoard: false,
+ allowSubEpics: false,
issuableType: TYPE_ISSUE,
isGroupBoard,
isApolloBoard: false,
@@ -509,117 +502,4 @@ describe('Board card component', () => {
expect(findLoadingIcon().exists()).toBe(true);
});
});
-
- describe('is an epic board', () => {
- const descendantCounts = {
- closedEpics: 0,
- closedIssues: 0,
- openedEpics: 0,
- openedIssues: 0,
- };
-
- const descendantWeightSum = {
- closedIssues: 0,
- openedIssues: 0,
- };
-
- beforeEach(() => {
- createStore();
- });
-
- it('should render if the item has issues', () => {
- createWrapper({
- props: {
- item: {
- ...issue,
- descendantCounts,
- descendantWeightSum,
- hasIssues: true,
- },
- },
- isEpicBoard: true,
- });
-
- expect(findEpicCountables().exists()).toBe(true);
- });
-
- it('should not render if the item does not have issues', () => {
- createWrapper({
- item: {
- ...issue,
- descendantCounts,
- descendantWeightSum,
- hasIssues: false,
- },
- });
-
- expect(findEpicCountablesBadgeIssues().exists()).toBe(false);
- });
-
- it('shows render item countBadge, weights, and progress correctly', () => {
- createWrapper({
- props: {
- item: {
- ...issue,
- descendantCounts: {
- ...descendantCounts,
- openedIssues: 1,
- },
- descendantWeightSum: {
- closedIssues: 10,
- openedIssues: 5,
- },
- hasIssues: true,
- },
- },
- isEpicBoard: true,
- });
-
- expect(findEpicCountablesBadgeIssues().text()).toBe('1');
- expect(findEpicCountablesBadgeWeight().text()).toBe('15');
- expect(findEpicBadgeProgress().text()).toBe('67%');
- });
-
- it('does not render progress when weight is zero', () => {
- createWrapper({
- props: {
- item: {
- ...issue,
- descendantCounts: {
- ...descendantCounts,
- openedIssues: 1,
- },
- descendantWeightSum,
- hasIssues: true,
- },
- },
- isEpicBoard: true,
- });
-
- expect(findEpicBadgeProgress().exists()).toBe(false);
- });
-
- it('renders the tooltip with the correct data', () => {
- createWrapper({
- props: {
- item: {
- ...issue,
- descendantCounts,
- descendantWeightSum: {
- closedIssues: 10,
- openedIssues: 5,
- },
- hasIssues: true,
- },
- },
- isEpicBoard: true,
- });
-
- const tooltip = findEpicCountablesTotalTooltip();
- expect(tooltip).toBeDefined();
-
- expect(findEpicCountablesTotalWeight().text()).toBe('15');
- expect(findEpicProgressTooltip().text()).toBe('10 of 15 weight completed');
- });
- });
});
diff --git a/spec/frontend/boards/cache_updates_spec.js b/spec/frontend/boards/cache_updates_spec.js
index bc661f20451..07f5cef4a36 100644
--- a/spec/frontend/boards/cache_updates_spec.js
+++ b/spec/frontend/boards/cache_updates_spec.js
@@ -1,4 +1,4 @@
-import * as Sentry from '@sentry/browser';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import { setError } from '~/boards/graphql/cache_updates';
import { defaultClient } from '~/graphql_shared/issuable_client';
import setErrorMutation from '~/boards/graphql/client/set_error.mutation.graphql';
diff --git a/spec/frontend/boards/mock_data.js b/spec/frontend/boards/mock_data.js
index dfc8b18e197..0be17db9450 100644
--- a/spec/frontend/boards/mock_data.js
+++ b/spec/frontend/boards/mock_data.js
@@ -281,6 +281,7 @@ export const rawIssue = {
title: 'Issue 1',
id: 'gid://gitlab/Issue/436',
iid: '27',
+ closedAt: null,
dueDate: null,
timeEstimate: 0,
confidential: false,
@@ -324,6 +325,7 @@ export const mockIssue = {
id: 'gid://gitlab/Issue/436',
iid: '27',
title: 'Issue 1',
+ closedAt: null,
dueDate: null,
timeEstimate: 0,
confidential: false,
@@ -412,6 +414,7 @@ export const mockIssue2 = {
id: 'gid://gitlab/Issue/437',
iid: 28,
title: 'Issue 2',
+ closedAt: null,
dueDate: null,
timeEstimate: 0,
confidential: false,
diff --git a/spec/frontend/boards/stores/actions_spec.js b/spec/frontend/boards/stores/actions_spec.js
index 5b4b79c650a..358cb340802 100644
--- a/spec/frontend/boards/stores/actions_spec.js
+++ b/spec/frontend/boards/stores/actions_spec.js
@@ -1,8 +1,8 @@
-import * as Sentry from '@sentry/browser';
import { cloneDeep } from 'lodash';
import Vue from 'vue';
// eslint-disable-next-line no-restricted-imports
import Vuex from 'vuex';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import { inactiveId, ISSUABLE, ListType, DraggableItemTypes } from 'ee_else_ce/boards/constants';
import issueMoveListMutation from 'ee_else_ce/boards/graphql/issue_move_list.mutation.graphql';
import testAction from 'helpers/vuex_action_helper';
diff --git a/spec/frontend/ci/artifacts/components/job_artifacts_table_spec.js b/spec/frontend/ci/artifacts/components/job_artifacts_table_spec.js
index 3628af31aa1..ba77d90f4e2 100644
--- a/spec/frontend/ci/artifacts/components/job_artifacts_table_spec.js
+++ b/spec/frontend/ci/artifacts/components/job_artifacts_table_spec.js
@@ -2,7 +2,7 @@ import { GlLoadingIcon, GlTable, GlLink, GlPagination, GlModal, GlFormCheckbox }
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import getJobArtifactsResponse from 'test_fixtures/graphql/ci/artifacts/graphql/queries/get_job_artifacts.query.graphql.json';
-import CiBadgeLink from '~/vue_shared/components/ci_badge_link.vue';
+import CiIcon from '~/vue_shared/components/ci_icon.vue';
import waitForPromises from 'helpers/wait_for_promises';
import JobArtifactsTable from '~/ci/artifacts/components/job_artifacts_table.vue';
import ArtifactsTableRowDetails from '~/ci/artifacts/components/artifacts_table_row_details.vue';
@@ -51,7 +51,7 @@ describe('JobArtifactsTable component', () => {
const findStatuses = () => wrapper.findAllByTestId('job-artifacts-job-status');
const findSuccessfulJobStatus = () => findStatuses().at(0);
- const findCiBadgeLink = () => findSuccessfulJobStatus().findComponent(CiBadgeLink);
+ const findCiIcon = () => findSuccessfulJobStatus().findComponent(CiIcon);
const findLinks = () => wrapper.findAllComponents(GlLink);
const findJobLink = () => findLinks().at(0);
@@ -201,12 +201,11 @@ describe('JobArtifactsTable component', () => {
});
it('shows the job status as an icon for a successful job', () => {
- expect(findCiBadgeLink().props()).toMatchObject({
+ expect(findCiIcon().props()).toMatchObject({
status: {
group: 'success',
},
- size: 'sm',
- showText: false,
+ showStatusText: false,
});
});
diff --git a/spec/frontend/ci/catalog/components/details/ci_resource_components_spec.js b/spec/frontend/ci/catalog/components/details/ci_resource_components_spec.js
index a41996d20b3..382f8e46203 100644
--- a/spec/frontend/ci/catalog/components/details/ci_resource_components_spec.js
+++ b/spec/frontend/ci/catalog/components/details/ci_resource_components_spec.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
import VueApollo from 'vue-apollo';
-import { GlLoadingIcon } from '@gitlab/ui';
+import { GlEmptyState, GlLoadingIcon } from '@gitlab/ui';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import { resolvers } from '~/ci/catalog/graphql/settings';
import CiResourceComponents from '~/ci/catalog/components/details/ci_resource_components.vue';
@@ -8,7 +8,7 @@ import getCiCatalogcomponentComponents from '~/ci/catalog/graphql/queries/get_ci
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { createAlert } from '~/alert';
-import { mockComponents } from '../../mock';
+import { mockComponents, mockComponentsEmpty } from '../../mock';
Vue.use(VueApollo);
jest.mock('~/alert');
@@ -37,7 +37,9 @@ describe('CiResourceComponents', () => {
await waitForPromises();
};
+ const findEmptyState = () => wrapper.findComponent(GlEmptyState);
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
+ const findCopyToClipboardButton = (i) => wrapper.findAllByTestId('copy-to-clipboard').at(i);
const findComponents = () => wrapper.findAllByTestId('component-section');
beforeEach(() => {
@@ -82,30 +84,61 @@ describe('CiResourceComponents', () => {
});
describe('when queries have loaded', () => {
- beforeEach(async () => {
- await createComponent();
- });
+ describe('and there is no metadata', () => {
+ beforeEach(async () => {
+ mockComponentsResponse.mockResolvedValue(mockComponentsEmpty);
+ await createComponent();
+ });
- it('renders every component', () => {
- expect(findComponents()).toHaveLength(components.length);
- });
+ it('renders the empty state', () => {
+ expect(findEmptyState().exists()).toBe(true);
+ expect(findEmptyState().props().title).toBe('Component details not available');
+ });
- it('renders the component name, description and snippet', () => {
- components.forEach((component) => {
- expect(wrapper.text()).toContain(component.name);
- expect(wrapper.text()).toContain(component.description);
- expect(wrapper.text()).toContain(component.path);
+ it('does not render components', () => {
+ expect(findComponents()).toHaveLength(0);
});
});
- describe('inputs', () => {
- it('renders the component parameter attributes', () => {
- const [firstComponent] = components;
+ describe('and there is metadata', () => {
+ beforeEach(async () => {
+ await createComponent();
+ });
+
+ it('does not render the empty state', () => {
+ expect(findEmptyState().exists()).toBe(false);
+ });
+
+ it('renders every component', () => {
+ expect(findComponents()).toHaveLength(components.length);
+ });
+
+ it('renders the component name, description and snippet', () => {
+ components.forEach((component) => {
+ expect(wrapper.text()).toContain(component.name);
+ expect(wrapper.text()).toContain(component.description);
+ expect(wrapper.text()).toContain(component.path);
+ });
+ });
+
+ it('adds a copy-to-clipboard button', () => {
+ components.forEach((component, i) => {
+ const button = findCopyToClipboardButton(i);
+
+ expect(button.props().icon).toBe('copy-to-clipboard');
+ expect(button.attributes('data-clipboard-text')).toContain(component.path);
+ });
+ });
+
+ describe('inputs', () => {
+ it('renders the component parameter attributes', () => {
+ const [firstComponent] = components;
- firstComponent.inputs.nodes.forEach((input) => {
- expect(findComponents().at(0).text()).toContain(input.name);
- expect(findComponents().at(0).text()).toContain(input.defaultValue);
- expect(findComponents().at(0).text()).toContain('Yes');
+ firstComponent.inputs.nodes.forEach((input) => {
+ expect(findComponents().at(0).text()).toContain(input.name);
+ expect(findComponents().at(0).text()).toContain(input.defaultValue);
+ expect(findComponents().at(0).text()).toContain('Yes');
+ });
});
});
});
diff --git a/spec/frontend/ci/catalog/components/details/ci_resource_header_spec.js b/spec/frontend/ci/catalog/components/details/ci_resource_header_spec.js
index 6ab9520508d..c061332ba13 100644
--- a/spec/frontend/ci/catalog/components/details/ci_resource_header_spec.js
+++ b/spec/frontend/ci/catalog/components/details/ci_resource_header_spec.js
@@ -2,8 +2,8 @@ import { GlAvatar, GlAvatarLink, GlBadge } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import CiResourceHeader from '~/ci/catalog/components/details/ci_resource_header.vue';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
-import CiBadgeLink from '~/vue_shared/components/ci_badge_link.vue';
import CiResourceAbout from '~/ci/catalog/components/details/ci_resource_about.vue';
+import CiIcon from '~/vue_shared/components/ci_icon.vue';
import { catalogSharedDataMock, catalogAdditionalDetailsMock } from '../../mock';
describe('CiResourceHeader', () => {
@@ -24,7 +24,7 @@ describe('CiResourceHeader', () => {
const findAvatar = () => wrapper.findComponent(GlAvatar);
const findAvatarLink = () => wrapper.findComponent(GlAvatarLink);
const findVersionBadge = () => wrapper.findComponent(GlBadge);
- const findPipelineStatusBadge = () => wrapper.findComponent(CiBadgeLink);
+ const findPipelineStatusBadge = () => wrapper.findComponent(CiIcon);
const createComponent = ({ props = {} } = {}) => {
wrapper = shallowMountExtended(CiResourceHeader, {
@@ -126,8 +126,7 @@ describe('CiResourceHeader', () => {
expect(findPipelineStatusBadge().exists()).toBe(hasPipelineBadge);
if (hasPipelineBadge) {
expect(findPipelineStatusBadge().props()).toEqual({
- showText: true,
- size: 'sm',
+ showStatusText: true,
status: pipelineStatus,
showTooltip: true,
useLink: true,
diff --git a/spec/frontend/ci/catalog/components/list/catalog_header_spec.js b/spec/frontend/ci/catalog/components/list/catalog_header_spec.js
index 912fd9e1a93..2a5c24d0515 100644
--- a/spec/frontend/ci/catalog/components/list/catalog_header_spec.js
+++ b/spec/frontend/ci/catalog/components/list/catalog_header_spec.js
@@ -10,36 +10,53 @@ describe('CatalogHeader', () => {
let wrapper;
const defaultProps = {};
- const defaultProvide = {
+ const customProvide = {
pageTitle: 'Catalog page',
pageDescription: 'This is a nice catalog page',
};
const findBanner = () => wrapper.findComponent(GlBanner);
const findFeedbackButton = () => findBanner().findComponent(GlButton);
- const findTitle = () => wrapper.findByText(defaultProvide.pageTitle);
- const findDescription = () => wrapper.findByText(defaultProvide.pageDescription);
+ const findTitle = () => wrapper.find('h1');
+ const findDescription = () => wrapper.findByTestId('description');
- const createComponent = ({ props = {}, stubs = {} } = {}) => {
+ const createComponent = ({ props = {}, provide = {}, stubs = {} } = {}) => {
wrapper = shallowMountExtended(CatalogHeader, {
propsData: {
...defaultProps,
...props,
},
- provide: defaultProvide,
+ provide,
stubs: {
...stubs,
},
});
};
- it('renders the Catalog title and description', () => {
- createComponent();
+ describe('title and description', () => {
+ describe('when there are no values provided', () => {
+ beforeEach(() => {
+ createComponent();
+ });
- expect(findTitle().exists()).toBe(true);
- expect(findDescription().exists()).toBe(true);
- });
+ it('renders the default values', () => {
+ expect(findTitle().text()).toBe('CI/CD Catalog');
+ expect(findDescription().text()).toBe(
+ 'Discover CI configuration resources for a seamless CI/CD experience.',
+ );
+ });
+ });
+ describe('when custom values are provided', () => {
+ beforeEach(() => {
+ createComponent({ provide: customProvide });
+ });
+ it('renders the custom values', () => {
+ expect(findTitle().text()).toBe(customProvide.pageTitle);
+ expect(findDescription().text()).toBe(customProvide.pageDescription);
+ });
+ });
+ });
describe('Feedback banner', () => {
describe('when user has never dismissed', () => {
beforeEach(() => {
diff --git a/spec/frontend/ci/catalog/components/list/ci_resources_list_item_spec.js b/spec/frontend/ci/catalog/components/list/ci_resources_list_item_spec.js
index 7f446064366..3862195d8c7 100644
--- a/spec/frontend/ci/catalog/components/list/ci_resources_list_item_spec.js
+++ b/spec/frontend/ci/catalog/components/list/ci_resources_list_item_spec.js
@@ -48,7 +48,6 @@ describe('CiResourcesListItem', () => {
const findUserLink = () => wrapper.findByTestId('user-link');
const findTimeAgoMessage = () => wrapper.findComponent(GlSprintf);
const findFavorites = () => wrapper.findByTestId('stats-favorites');
- const findForks = () => wrapper.findByTestId('stats-forks');
beforeEach(() => {
router = createRouter();
@@ -161,7 +160,6 @@ describe('CiResourcesListItem', () => {
createComponent({
props: {
resource: {
- forksCount: 0,
starCount: 0,
},
},
@@ -172,11 +170,6 @@ describe('CiResourcesListItem', () => {
expect(findFavorites().exists()).toBe(true);
expect(findFavorites().text()).toBe('0');
});
-
- it('render forks as 0', () => {
- expect(findForks().exists()).toBe(true);
- expect(findForks().text()).toBe('0');
- });
});
describe('where there are statistics', () => {
@@ -188,11 +181,6 @@ describe('CiResourcesListItem', () => {
expect(findFavorites().exists()).toBe(true);
expect(findFavorites().text()).toBe(String(defaultProps.resource.starCount));
});
-
- it('render forks', () => {
- expect(findForks().exists()).toBe(true);
- expect(findForks().text()).toBe(String(defaultProps.resource.forksCount));
- });
});
});
});
diff --git a/spec/frontend/ci/catalog/components/pages/ci_resources_page_spec.js b/spec/frontend/ci/catalog/components/pages/ci_resources_page_spec.js
new file mode 100644
index 00000000000..e18b418b155
--- /dev/null
+++ b/spec/frontend/ci/catalog/components/pages/ci_resources_page_spec.js
@@ -0,0 +1,211 @@
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import { createAlert } from '~/alert';
+
+import CatalogHeader from '~/ci/catalog/components/list/catalog_header.vue';
+import CiResourcesList from '~/ci/catalog/components/list/ci_resources_list.vue';
+import CatalogListSkeletonLoader from '~/ci/catalog/components/list/catalog_list_skeleton_loader.vue';
+import EmptyState from '~/ci/catalog/components/list/empty_state.vue';
+import { cacheConfig } from '~/ci/catalog/graphql/settings';
+import ciResourcesPage from '~/ci/catalog/components/pages/ci_resources_page.vue';
+
+import getCatalogResources from '~/ci/catalog/graphql/queries/get_ci_catalog_resources.query.graphql';
+
+import { emptyCatalogResponseBody, catalogResponseBody } from '../../mock';
+
+Vue.use(VueApollo);
+jest.mock('~/alert');
+
+describe('CiResourcesPage', () => {
+ let wrapper;
+ let catalogResourcesResponse;
+
+ const createComponent = () => {
+ const handlers = [[getCatalogResources, catalogResourcesResponse]];
+ const mockApollo = createMockApollo(handlers, {}, cacheConfig);
+
+ wrapper = shallowMountExtended(ciResourcesPage, {
+ apolloProvider: mockApollo,
+ });
+
+ return waitForPromises();
+ };
+
+ const findCatalogHeader = () => wrapper.findComponent(CatalogHeader);
+ const findCiResourcesList = () => wrapper.findComponent(CiResourcesList);
+ const findLoadingState = () => wrapper.findComponent(CatalogListSkeletonLoader);
+ const findEmptyState = () => wrapper.findComponent(EmptyState);
+
+ beforeEach(() => {
+ catalogResourcesResponse = jest.fn();
+ });
+
+ describe('when initial queries are loading', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('shows a loading icon and no list', () => {
+ expect(findLoadingState().exists()).toBe(true);
+ expect(findEmptyState().exists()).toBe(false);
+ expect(findCiResourcesList().exists()).toBe(false);
+ });
+ });
+
+ describe('when queries have loaded', () => {
+ it('renders the Catalog Header', async () => {
+ await createComponent();
+
+ expect(findCatalogHeader().exists()).toBe(true);
+ });
+
+ describe('and there are no resources', () => {
+ beforeEach(async () => {
+ catalogResourcesResponse.mockResolvedValue(emptyCatalogResponseBody);
+
+ await createComponent();
+ });
+
+ it('renders the empty state', () => {
+ expect(findLoadingState().exists()).toBe(false);
+ expect(findEmptyState().exists()).toBe(true);
+ expect(findCiResourcesList().exists()).toBe(false);
+ });
+ });
+
+ describe('and there are resources', () => {
+ const { nodes, pageInfo, count } = catalogResponseBody.data.ciCatalogResources;
+
+ beforeEach(async () => {
+ catalogResourcesResponse.mockResolvedValue(catalogResponseBody);
+
+ await createComponent();
+ });
+ it('renders the resources list', () => {
+ expect(findLoadingState().exists()).toBe(false);
+ expect(findEmptyState().exists()).toBe(false);
+ expect(findCiResourcesList().exists()).toBe(true);
+ });
+
+ it('passes down props to the resources list', () => {
+ expect(findCiResourcesList().props()).toMatchObject({
+ currentPage: 1,
+ resources: nodes,
+ pageInfo,
+ totalCount: count,
+ });
+ });
+ });
+ });
+
+ describe('pagination', () => {
+ it.each`
+ eventName
+ ${'onPrevPage'}
+ ${'onNextPage'}
+ `('refetch query with new params when receiving $eventName', async ({ eventName }) => {
+ const { pageInfo } = catalogResponseBody.data.ciCatalogResources;
+
+ catalogResourcesResponse.mockResolvedValue(catalogResponseBody);
+ await createComponent();
+
+ expect(catalogResourcesResponse).toHaveBeenCalledTimes(1);
+
+ await findCiResourcesList().vm.$emit(eventName);
+
+ expect(catalogResourcesResponse).toHaveBeenCalledTimes(2);
+
+ if (eventName === 'onNextPage') {
+ expect(catalogResourcesResponse.mock.calls[1][0]).toEqual({
+ after: pageInfo.endCursor,
+ first: 20,
+ });
+ } else {
+ expect(catalogResourcesResponse.mock.calls[1][0]).toEqual({
+ before: pageInfo.startCursor,
+ last: 20,
+ first: null,
+ });
+ }
+ });
+ });
+
+ describe('pages count', () => {
+ describe('when the fetchMore call suceeds', () => {
+ beforeEach(async () => {
+ catalogResourcesResponse.mockResolvedValue(catalogResponseBody);
+
+ await createComponent();
+ });
+
+ it('increments and drecrements the page count correctly', async () => {
+ expect(findCiResourcesList().props().currentPage).toBe(1);
+
+ findCiResourcesList().vm.$emit('onNextPage');
+ await waitForPromises();
+
+ expect(findCiResourcesList().props().currentPage).toBe(2);
+
+ await findCiResourcesList().vm.$emit('onPrevPage');
+ await waitForPromises();
+
+ expect(findCiResourcesList().props().currentPage).toBe(1);
+ });
+ });
+
+ describe('when the fetchMore call fails', () => {
+ const errorMessage = 'there was an error';
+
+ describe('for next page', () => {
+ beforeEach(async () => {
+ catalogResourcesResponse.mockResolvedValueOnce(catalogResponseBody);
+ catalogResourcesResponse.mockRejectedValue({ message: errorMessage });
+
+ await createComponent();
+ });
+
+ it('does not increment the page and calls createAlert', async () => {
+ expect(findCiResourcesList().props().currentPage).toBe(1);
+
+ findCiResourcesList().vm.$emit('onNextPage');
+ await waitForPromises();
+
+ expect(findCiResourcesList().props().currentPage).toBe(1);
+ expect(createAlert).toHaveBeenCalledWith({ message: errorMessage, variant: 'danger' });
+ });
+ });
+
+ describe('for previous page', () => {
+ beforeEach(async () => {
+ // Initial query
+ catalogResourcesResponse.mockResolvedValueOnce(catalogResponseBody);
+ // When clicking on next
+ catalogResourcesResponse.mockResolvedValueOnce(catalogResponseBody);
+ // when clicking on previous
+ catalogResourcesResponse.mockRejectedValue({ message: errorMessage });
+
+ await createComponent();
+ });
+
+ it('does not decrement the page and calls createAlert', async () => {
+ expect(findCiResourcesList().props().currentPage).toBe(1);
+
+ findCiResourcesList().vm.$emit('onNextPage');
+ await waitForPromises();
+
+ expect(findCiResourcesList().props().currentPage).toBe(2);
+
+ findCiResourcesList().vm.$emit('onPrevPage');
+ await waitForPromises();
+
+ expect(findCiResourcesList().props().currentPage).toBe(2);
+ expect(createAlert).toHaveBeenCalledWith({ message: errorMessage, variant: 'danger' });
+ });
+ });
+ });
+ });
+});
diff --git a/spec/frontend/ci/catalog/global_catalog_spec.js b/spec/frontend/ci/catalog/global_catalog_spec.js
new file mode 100644
index 00000000000..fddabf46c0b
--- /dev/null
+++ b/spec/frontend/ci/catalog/global_catalog_spec.js
@@ -0,0 +1,17 @@
+import { shallowMount } from '@vue/test-utils';
+import GlobalCatalog from '~/ci/catalog/global_catalog.vue';
+import CiCatalogHome from '~/ci/catalog/components/ci_catalog_home.vue';
+
+describe('GlobalCatalog', () => {
+ let wrapper;
+
+ const findHomeComponent = () => wrapper.findComponent(CiCatalogHome);
+
+ beforeEach(() => {
+ wrapper = shallowMount(GlobalCatalog);
+ });
+
+ it('renders the catalog home component', () => {
+ expect(findHomeComponent().exists()).toBe(true);
+ });
+});
diff --git a/spec/frontend/ci/catalog/index_spec.js b/spec/frontend/ci/catalog/index_spec.js
new file mode 100644
index 00000000000..01332cfbb3d
--- /dev/null
+++ b/spec/frontend/ci/catalog/index_spec.js
@@ -0,0 +1,48 @@
+import Vue from 'vue';
+import { initCatalog } from '~/ci/catalog/';
+import * as Router from '~/ci/catalog/router';
+import CiResourcesPage from '~/ci/catalog/components/pages/ci_resources_page.vue';
+
+describe('~/ci/catalog/index', () => {
+ describe('initCatalog', () => {
+ const SELECTOR = 'SELECTOR';
+
+ let el;
+ let component;
+ const baseRoute = '/explore/catalog';
+
+ const createElement = () => {
+ el = document.createElement('div');
+ el.id = SELECTOR;
+ el.dataset.ciCatalogPath = baseRoute;
+ document.body.appendChild(el);
+ };
+
+ afterEach(() => {
+ el = null;
+ });
+
+ describe('when the element exists', () => {
+ beforeEach(() => {
+ createElement();
+ jest.spyOn(Router, 'createRouter');
+ component = initCatalog(`#${SELECTOR}`);
+ });
+
+ it('returns a Vue Instance', () => {
+ expect(component).toBeInstanceOf(Vue);
+ });
+
+ it('creates a router with the received base path and component', () => {
+ expect(Router.createRouter).toHaveBeenCalledTimes(1);
+ expect(Router.createRouter).toHaveBeenCalledWith(baseRoute, CiResourcesPage);
+ });
+ });
+
+ describe('When the element does not exist', () => {
+ it('returns `null`', () => {
+ expect(initCatalog('foo')).toBe(null);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/ci/catalog/mock.js b/spec/frontend/ci/catalog/mock.js
index 21fed6ac8ec..125f003224c 100644
--- a/spec/frontend/ci/catalog/mock.js
+++ b/spec/frontend/ci/catalog/mock.js
@@ -1,5 +1,23 @@
import { componentsMockData } from '~/ci/catalog/constants';
+export const emptyCatalogResponseBody = {
+ data: {
+ ciCatalogResources: {
+ pageInfo: {
+ startCursor:
+ 'eyJjcmVhdGVkX2F0IjoiMjAxNS0wNy0wMyAxMDowMDowMC4wMDAwMDAwMDAgKzAwMDAiLCJpZCI6IjEyOSJ9',
+ endCursor:
+ 'eyJjcmVhdGVkX2F0IjoiMjAxNS0wNy0wMyAxMDowMDowMC4wMDAwMDAwMDAgKzAwMDAiLCJpZCI6IjExMCJ9',
+ hasNextPage: false,
+ hasPreviousPage: false,
+ __typename: 'PageInfo',
+ },
+ count: 0,
+ nodes: [],
+ },
+ },
+};
+
export const catalogResponseBody = {
data: {
ciCatalogResources: {
@@ -20,7 +38,6 @@ export const catalogResponseBody = {
name: 'Project-42 Name',
description: 'A simple component',
starCount: 0,
- forksCount: 0,
latestVersion: null,
rootNamespace: {
id: 'gid://gitlab/Group/185',
@@ -37,7 +54,6 @@ export const catalogResponseBody = {
name: 'Project-41 Name',
description: 'A simple component',
starCount: 0,
- forksCount: 0,
latestVersion: null,
rootNamespace: {
id: 'gid://gitlab/Group/185',
@@ -54,7 +70,6 @@ export const catalogResponseBody = {
name: 'Project-40 Name',
description: 'A simple component',
starCount: 0,
- forksCount: 0,
latestVersion: null,
rootNamespace: {
id: 'gid://gitlab/Group/185',
@@ -71,7 +86,6 @@ export const catalogResponseBody = {
name: 'Project-39 Name',
description: 'A simple component',
starCount: 0,
- forksCount: 0,
latestVersion: null,
rootNamespace: {
id: 'gid://gitlab/Group/185',
@@ -88,7 +102,6 @@ export const catalogResponseBody = {
name: 'Project-38 Name',
description: 'A simple component',
starCount: 0,
- forksCount: 0,
latestVersion: null,
rootNamespace: {
id: 'gid://gitlab/Group/185',
@@ -105,7 +118,6 @@ export const catalogResponseBody = {
name: 'Project-37 Name',
description: 'A simple component',
starCount: 0,
- forksCount: 0,
latestVersion: null,
rootNamespace: {
id: 'gid://gitlab/Group/185',
@@ -122,7 +134,6 @@ export const catalogResponseBody = {
name: 'Project-36 Name',
description: 'A simple component',
starCount: 0,
- forksCount: 0,
latestVersion: null,
rootNamespace: {
id: 'gid://gitlab/Group/185',
@@ -139,7 +150,6 @@ export const catalogResponseBody = {
name: 'Project-35 Name',
description: 'A simple component',
starCount: 0,
- forksCount: 0,
latestVersion: null,
rootNamespace: {
id: 'gid://gitlab/Group/185',
@@ -156,7 +166,6 @@ export const catalogResponseBody = {
name: 'Project-34 Name',
description: 'A simple component',
starCount: 0,
- forksCount: 0,
latestVersion: null,
rootNamespace: {
id: 'gid://gitlab/Group/185',
@@ -173,7 +182,6 @@ export const catalogResponseBody = {
name: 'Project-33 Name',
description: 'A simple component',
starCount: 0,
- forksCount: 0,
latestVersion: null,
rootNamespace: {
id: 'gid://gitlab/Group/185',
@@ -190,7 +198,6 @@ export const catalogResponseBody = {
name: 'Project-32 Name',
description: 'A simple component',
starCount: 0,
- forksCount: 0,
latestVersion: null,
rootNamespace: {
id: 'gid://gitlab/Group/185',
@@ -207,7 +214,6 @@ export const catalogResponseBody = {
name: 'Project-31 Name',
description: 'A simple component',
starCount: 0,
- forksCount: 0,
latestVersion: null,
rootNamespace: {
id: 'gid://gitlab/Group/185',
@@ -224,7 +230,6 @@ export const catalogResponseBody = {
name: 'Project-30 Name',
description: 'A simple component',
starCount: 0,
- forksCount: 0,
latestVersion: null,
rootNamespace: {
id: 'gid://gitlab/Group/185',
@@ -241,7 +246,6 @@ export const catalogResponseBody = {
name: 'Project-29 Name',
description: 'A simple component',
starCount: 0,
- forksCount: 0,
latestVersion: null,
rootNamespace: {
id: 'gid://gitlab/Group/185',
@@ -258,7 +262,6 @@ export const catalogResponseBody = {
name: 'Project-28 Name',
description: 'A simple component',
starCount: 0,
- forksCount: 0,
latestVersion: null,
rootNamespace: {
id: 'gid://gitlab/Group/185',
@@ -275,7 +278,6 @@ export const catalogResponseBody = {
name: 'Project-27 Name',
description: 'A simple component',
starCount: 0,
- forksCount: 0,
latestVersion: null,
rootNamespace: {
id: 'gid://gitlab/Group/185',
@@ -292,7 +294,6 @@ export const catalogResponseBody = {
name: 'Project-26 Name',
description: 'A simple component',
starCount: 0,
- forksCount: 0,
latestVersion: null,
rootNamespace: {
id: 'gid://gitlab/Group/185',
@@ -309,7 +310,6 @@ export const catalogResponseBody = {
name: 'Project-25 Name',
description: 'A simple component',
starCount: 0,
- forksCount: 0,
latestVersion: null,
rootNamespace: {
id: 'gid://gitlab/Group/185',
@@ -326,7 +326,6 @@ export const catalogResponseBody = {
name: 'Project-24 Name',
description: 'A simple component',
starCount: 0,
- forksCount: 0,
latestVersion: null,
rootNamespace: {
id: 'gid://gitlab/Group/185',
@@ -343,7 +342,6 @@ export const catalogResponseBody = {
name: 'Project-23 Name',
description: 'A simple component',
starCount: 0,
- forksCount: 0,
latestVersion: null,
rootNamespace: {
id: 'gid://gitlab/Group/185',
@@ -380,7 +378,6 @@ export const catalogSinglePageResponse = {
name: 'Project-45 Name',
description: 'A simple component',
starCount: 0,
- forksCount: 0,
latestVersion: null,
rootNamespace: {
id: 'gid://gitlab/Group/185',
@@ -397,7 +394,6 @@ export const catalogSinglePageResponse = {
name: 'Project-44 Name',
description: 'A simple component',
starCount: 0,
- forksCount: 0,
latestVersion: null,
rootNamespace: {
id: 'gid://gitlab/Group/185',
@@ -414,7 +410,6 @@ export const catalogSinglePageResponse = {
name: 'Project-43 Name',
description: 'A simple component',
starCount: 0,
- forksCount: 0,
latestVersion: null,
rootNamespace: {
id: 'gid://gitlab/Group/185',
@@ -441,7 +436,6 @@ export const catalogSharedDataMock = {
name: 'Ruby',
rootNamespace: { id: 1, fullPath: '/group/project', name: 'my-dumb-project' },
starCount: 1,
- forksCount: 2,
latestVersion: {
__typename: 'Release',
id: '3',
@@ -506,7 +500,6 @@ const generateResourcesNodes = (count = 20, startId = 0) => {
__typename: 'CiCatalogResource',
id: `gid://gitlab/CiCatalogResource/${i}`,
description: `This is a component that does a bunch of stuff and is really just a number: ${i}`,
- forksCount: 5,
icon: 'my-icon',
name: `My component #${i}`,
rootNamespace: {
@@ -544,3 +537,13 @@ export const mockComponents = {
},
},
};
+
+export const mockComponentsEmpty = {
+ data: {
+ ciCatalogResource: {
+ __typename: 'CiCatalogResource',
+ id: `gid://gitlab/CiCatalogResource/1`,
+ components: [],
+ },
+ },
+};
diff --git a/spec/frontend/ci/ci_variable_list/components/ci_variable_drawer_spec.js b/spec/frontend/ci/ci_variable_list/components/ci_variable_drawer_spec.js
index 207ea7aa060..610aae3946f 100644
--- a/spec/frontend/ci/ci_variable_list/components/ci_variable_drawer_spec.js
+++ b/spec/frontend/ci/ci_variable_list/components/ci_variable_drawer_spec.js
@@ -67,9 +67,9 @@ describe('CI Variable Drawer', () => {
});
};
- const findConfirmBtn = () => wrapper.findByTestId('ci-variable-confirm-btn');
+ const findConfirmBtn = () => wrapper.findByTestId('ci-variable-confirm-button');
const findConfirmDeleteModal = () => wrapper.findComponent(GlModal);
- const findDeleteBtn = () => wrapper.findByTestId('ci-variable-delete-btn');
+ const findDeleteBtn = () => wrapper.findByTestId('ci-variable-delete-button');
const findDisabledEnvironmentScopeDropdown = () => wrapper.findComponent(GlFormInput);
const findDrawer = () => wrapper.findComponent(GlDrawer);
const findEnvironmentScopeDropdown = () => wrapper.findComponent(CiEnvironmentsDropdown);
@@ -350,6 +350,13 @@ describe('CI Variable Drawer', () => {
});
describe('drawer events', () => {
+ it('emits `search-environment-scope` before mounting', () => {
+ createComponent();
+
+ expect(wrapper.emitted('search-environment-scope')).toHaveLength(1);
+ expect(wrapper.emitted('search-environment-scope')).toEqual([['']]);
+ });
+
it('emits `close-form` when closing the drawer', async () => {
createComponent();
@@ -477,7 +484,7 @@ describe('CI Variable Drawer', () => {
it('bubbles up the search event', async () => {
await findEnvironmentScopeDropdown().vm.$emit('search-environment-scope', 'staging');
- expect(wrapper.emitted('search-environment-scope')).toEqual([['staging']]);
+ expect(wrapper.emitted('search-environment-scope')[1]).toEqual(['staging']);
});
});
});
diff --git a/spec/frontend/ci/ci_variable_list/components/ci_variable_modal_spec.js b/spec/frontend/ci/ci_variable_list/components/ci_variable_modal_spec.js
deleted file mode 100644
index 5ba9b3b8c20..00000000000
--- a/spec/frontend/ci/ci_variable_list/components/ci_variable_modal_spec.js
+++ /dev/null
@@ -1,576 +0,0 @@
-import { GlButton, GlFormInput, GlSprintf } from '@gitlab/ui';
-import { mockTracking } from 'helpers/tracking_helper';
-import { shallowMountExtended, mountExtended } from 'helpers/vue_test_utils_helper';
-import CiEnvironmentsDropdown from '~/ci/ci_variable_list/components/ci_environments_dropdown.vue';
-import CiVariableModal from '~/ci/ci_variable_list/components/ci_variable_modal.vue';
-import {
- ADD_VARIABLE_ACTION,
- AWS_ACCESS_KEY_ID,
- EDIT_VARIABLE_ACTION,
- EVENT_LABEL,
- EVENT_ACTION,
- ENVIRONMENT_SCOPE_LINK_TITLE,
- AWS_TIP_TITLE,
- AWS_TIP_MESSAGE,
- instanceString,
- variableOptions,
-} from '~/ci/ci_variable_list/constants';
-import { mockVariablesWithScopes } from '../mocks';
-import ModalStub from '../stubs';
-
-describe('Ci variable modal', () => {
- let wrapper;
- let trackingSpy;
-
- const maskableRegex = '^[a-zA-Z0-9_+=/@:.~-]{8,}$';
- const maskableRawRegex = '^\\S{8,}$';
-
- const mockVariables = mockVariablesWithScopes(instanceString);
-
- const defaultProvide = {
- containsVariableReferenceLink: '/reference',
- environmentScopeLink: '/help/environments',
- glFeatures: {
- ciRemoveCharacterLimitationRawMaskedVar: true,
- },
- isProtectedByDefault: false,
- maskedEnvironmentVariablesLink: '/variables-link',
- maskableRawRegex,
- maskableRegex,
- };
-
- const defaultProps = {
- areEnvironmentsLoading: false,
- areScopedVariablesAvailable: true,
- environments: [],
- hideEnvironmentScope: false,
- mode: ADD_VARIABLE_ACTION,
- selectedVariable: {},
- variables: [],
- };
-
- const createComponent = ({ mountFn = shallowMountExtended, props = {}, provide = {} } = {}) => {
- wrapper = mountFn(CiVariableModal, {
- attachTo: document.body,
- provide: { ...defaultProvide, ...provide },
- propsData: {
- ...defaultProps,
- ...props,
- },
- stubs: {
- GlModal: ModalStub,
- },
- });
- };
-
- const findCiEnvironmentsDropdown = () => wrapper.findComponent(CiEnvironmentsDropdown);
- const findReferenceWarning = () => wrapper.findByTestId('contains-variable-reference');
- const findModal = () => wrapper.findComponent(ModalStub);
- const findAWSTip = () => wrapper.findByTestId('aws-guidance-tip');
- const findAddorUpdateButton = () => wrapper.findByTestId('ciUpdateOrAddVariableBtn');
- const deleteVariableButton = () =>
- findModal()
- .findAllComponents(GlButton)
- .wrappers.find((button) => button.props('variant') === 'danger');
- const findExpandedVariableCheckbox = () => wrapper.findByTestId('ci-variable-expanded-checkbox');
- const findProtectedVariableCheckbox = () =>
- wrapper.findByTestId('ci-variable-protected-checkbox');
- const findMaskedVariableCheckbox = () => wrapper.findByTestId('ci-variable-masked-checkbox');
- const findValueField = () => wrapper.find('#ci-variable-value');
- const findEnvScopeLink = () => wrapper.findByTestId('environment-scope-link');
- const findEnvScopeInput = () =>
- wrapper.findByTestId('environment-scope').findComponent(GlFormInput);
- const findRawVarTip = () => wrapper.findByTestId('raw-variable-tip');
- const findVariableTypeDropdown = () => wrapper.find('#ci-variable-type');
- const findEnvironmentScopeText = () => wrapper.findByText('Environment scope');
-
- describe('Adding a variable', () => {
- describe('when no key/value pair are present', () => {
- beforeEach(() => {
- createComponent();
- });
-
- it('shows the submit button as disabled', () => {
- expect(findAddorUpdateButton().attributes('disabled')).toBeDefined();
- });
- });
-
- describe('when a key/value pair is present', () => {
- beforeEach(() => {
- createComponent({ props: { selectedVariable: mockVariables[0] } });
- });
-
- it('shows the submit button as enabled', () => {
- expect(findAddorUpdateButton().attributes('disabled')).toBeUndefined();
- });
- });
-
- describe('events', () => {
- const [currentVariable] = mockVariables;
-
- beforeEach(() => {
- createComponent({ props: { selectedVariable: currentVariable } });
- });
-
- it('Dispatches `add-variable` action on submit', () => {
- findAddorUpdateButton().vm.$emit('click');
- expect(wrapper.emitted('add-variable')).toEqual([[currentVariable]]);
- });
-
- it('Dispatches the `close-form` event when dismissing', () => {
- findModal().vm.$emit('hidden');
- expect(wrapper.emitted('close-form')).toEqual([[]]);
- });
- });
- });
-
- describe('when protected by default', () => {
- describe('when adding a new variable', () => {
- beforeEach(() => {
- createComponent({ provide: { isProtectedByDefault: true } });
- findModal().vm.$emit('shown');
- });
-
- it('updates the protected value to true', () => {
- expect(findProtectedVariableCheckbox().attributes('data-is-protected-checked')).toBe(
- 'true',
- );
- });
- });
-
- describe('when editing a variable', () => {
- beforeEach(() => {
- createComponent({
- provide: { isProtectedByDefault: false },
- props: {
- selectedVariable: {},
- mode: EDIT_VARIABLE_ACTION,
- },
- });
- findModal().vm.$emit('shown');
- });
-
- it('keeps the value as false', () => {
- expect(
- findProtectedVariableCheckbox().attributes('data-is-protected-checked'),
- ).toBeUndefined();
- });
- });
- });
-
- describe('Adding a new non-AWS variable', () => {
- beforeEach(() => {
- const [variable] = mockVariables;
- createComponent({ mountFn: mountExtended, props: { selectedVariable: variable } });
- });
-
- it('does not show AWS guidance tip', () => {
- const tip = findAWSTip();
-
- expect(tip.isVisible()).toBe(false);
- });
- });
-
- describe('Adding a new AWS variable', () => {
- beforeEach(() => {
- const [variable] = mockVariables;
- const AWSKeyVariable = {
- ...variable,
- key: AWS_ACCESS_KEY_ID,
- value: 'AKIAIOSFODNN7EXAMPLEjdhy',
- };
- createComponent({
- mountFn: shallowMountExtended,
- props: { selectedVariable: AWSKeyVariable },
- });
- });
-
- it('shows AWS guidance tip', () => {
- const tip = findAWSTip();
-
- expect(tip.isVisible()).toBe(true);
- expect(tip.props('title')).toBe(AWS_TIP_TITLE);
- expect(tip.findComponent(GlSprintf).attributes('message')).toBe(AWS_TIP_MESSAGE);
- });
- });
-
- describe('when expanded', () => {
- describe('with a $ character', () => {
- beforeEach(() => {
- const [variable] = mockVariables;
- const variableWithDollarSign = {
- ...variable,
- value: 'valueWith$',
- };
- createComponent({
- mountFn: mountExtended,
- props: { selectedVariable: variableWithDollarSign },
- });
- });
-
- it(`renders the variable reference warning`, () => {
- expect(findReferenceWarning().exists()).toBe(true);
- });
-
- it(`does not render raw variable tip`, () => {
- expect(findRawVarTip().exists()).toBe(false);
- });
- });
-
- describe('without a $ character', () => {
- beforeEach(() => {
- const [variable] = mockVariables;
- createComponent({
- mountFn: mountExtended,
- props: { selectedVariable: variable },
- });
- });
-
- it(`does not render the variable reference warning`, () => {
- expect(findReferenceWarning().exists()).toBe(false);
- });
-
- it(`does not render raw variable tip`, () => {
- expect(findRawVarTip().exists()).toBe(false);
- });
- });
-
- describe('setting raw value', () => {
- const [variable] = mockVariables;
-
- it('defaults to expanded and raw:false when adding a variable', () => {
- createComponent({ props: { selectedVariable: variable } });
-
- findModal().vm.$emit('shown');
-
- expect(findExpandedVariableCheckbox().attributes('checked')).toBe('true');
-
- findAddorUpdateButton().vm.$emit('click');
-
- expect(wrapper.emitted('add-variable')).toEqual([
- [
- {
- ...variable,
- raw: false,
- },
- ],
- ]);
- });
-
- it('sets correct raw value when editing', async () => {
- createComponent({
- props: {
- selectedVariable: variable,
- mode: EDIT_VARIABLE_ACTION,
- },
- });
-
- findModal().vm.$emit('shown');
- await findExpandedVariableCheckbox().vm.$emit('change');
- await findAddorUpdateButton().vm.$emit('click');
-
- expect(wrapper.emitted('update-variable')).toEqual([
- [
- {
- ...variable,
- raw: true,
- },
- ],
- ]);
- });
- });
- });
-
- describe('when not expanded', () => {
- describe('with a $ character', () => {
- beforeEach(() => {
- const selectedVariable = mockVariables[1];
- createComponent({
- mountFn: mountExtended,
- props: { selectedVariable },
- });
- });
-
- it(`renders raw variable tip`, () => {
- expect(findRawVarTip().exists()).toBe(true);
- });
- });
- });
-
- describe('Editing a variable', () => {
- const [variable] = mockVariables;
-
- beforeEach(() => {
- createComponent({ props: { selectedVariable: variable, mode: EDIT_VARIABLE_ACTION } });
- });
-
- it('button text is Update variable when updating', () => {
- expect(findAddorUpdateButton().text()).toBe('Update variable');
- });
-
- it('Update variable button dispatches updateVariable with correct variable', () => {
- findAddorUpdateButton().vm.$emit('click');
- expect(wrapper.emitted('update-variable')).toEqual([[variable]]);
- });
-
- it('Propagates the `close-form` event', () => {
- findModal().vm.$emit('hidden');
- expect(wrapper.emitted('close-form')).toEqual([[]]);
- });
-
- it('dispatches `delete-variable` with correct variable to delete', () => {
- deleteVariableButton().vm.$emit('click');
- expect(wrapper.emitted('delete-variable')).toEqual([[variable]]);
- });
- });
-
- describe('Environment scope', () => {
- describe('when feature is available', () => {
- describe('and section is not hidden', () => {
- beforeEach(() => {
- createComponent({
- mountFn: mountExtended,
- props: {
- areScopedVariablesAvailable: true,
- hideEnvironmentScope: false,
- },
- });
- });
-
- it('renders the environment dropdown and section title', () => {
- expect(findCiEnvironmentsDropdown().exists()).toBe(true);
- expect(findCiEnvironmentsDropdown().isVisible()).toBe(true);
- expect(findEnvironmentScopeText().exists()).toBe(true);
- });
-
- it('renders a link to documentation on scopes', () => {
- const link = findEnvScopeLink();
-
- expect(link.attributes('title')).toBe(ENVIRONMENT_SCOPE_LINK_TITLE);
- expect(link.attributes('href')).toBe(defaultProvide.environmentScopeLink);
- });
- });
-
- describe('and section is hidden', () => {
- beforeEach(() => {
- createComponent({
- mountFn: mountExtended,
- props: {
- areScopedVariablesAvailable: true,
- hideEnvironmentScope: true,
- },
- });
- });
-
- it('does not renders the environment dropdown and section title', () => {
- expect(findCiEnvironmentsDropdown().exists()).toBe(false);
- expect(findEnvironmentScopeText().exists()).toBe(false);
- });
- });
- });
-
- describe('when feature is not available', () => {
- describe('and section is not hidden', () => {
- beforeEach(() => {
- createComponent({
- mountFn: mountExtended,
- props: {
- areScopedVariablesAvailable: false,
- hideEnvironmentScope: false,
- },
- });
- });
-
- it('disables the dropdown', () => {
- expect(findCiEnvironmentsDropdown().exists()).toBe(false);
- expect(findEnvironmentScopeText().exists()).toBe(true);
- expect(findEnvScopeInput().attributes('readonly')).toBe('readonly');
- });
- });
-
- describe('and section is hidden', () => {
- beforeEach(() => {
- createComponent({
- mountFn: mountExtended,
- props: {
- areScopedVariablesAvailable: false,
- hideEnvironmentScope: true,
- },
- });
- });
-
- it('hides the dropdown', () => {
- expect(findEnvironmentScopeText().exists()).toBe(false);
- expect(findCiEnvironmentsDropdown().exists()).toBe(false);
- });
- });
- });
- });
-
- describe('variable type dropdown', () => {
- describe('default behaviour', () => {
- beforeEach(() => {
- createComponent({ mountFn: mountExtended });
- });
-
- it('adds each option as a dropdown item', () => {
- expect(findVariableTypeDropdown().findAll('option')).toHaveLength(variableOptions.length);
- variableOptions.forEach((v) => {
- expect(findVariableTypeDropdown().text()).toContain(v.text);
- });
- });
- });
- });
-
- describe('Validations', () => {
- const maskError = 'This variable value does not meet the masking requirements.';
- const helpText = 'Value must meet regular expression requirements to be masked.';
-
- describe('when the variable is raw', () => {
- const [variable] = mockVariables;
- const validRawMaskedVariable = {
- ...variable,
- value: 'd$%^asdsadas',
- masked: false,
- raw: true,
- };
-
- beforeEach(() => {
- createComponent({
- mountFn: mountExtended,
- props: { selectedVariable: validRawMaskedVariable },
- });
- });
-
- it('should not show an error with symbols', async () => {
- await findMaskedVariableCheckbox().trigger('click');
-
- expect(findModal().text()).not.toContain(maskError);
- });
-
- it('should not show an error when length is less than 8', async () => {
- await findValueField().vm.$emit('input', 'a');
- await findMaskedVariableCheckbox().trigger('click');
-
- expect(findModal().text()).toContain(maskError);
- });
-
- it('does not show the masked variable help text', () => {
- expect(findModal().text()).not.toContain(helpText);
- });
- });
-
- describe('when the value is empty', () => {
- beforeEach(() => {
- const [variable] = mockVariables;
- const emptyValueVariable = { ...variable, value: '' };
- createComponent({
- mountFn: mountExtended,
- props: { selectedVariable: emptyValueVariable },
- });
- });
-
- it('allows user to submit', () => {
- expect(findAddorUpdateButton().attributes('disabled')).toBeUndefined();
- });
- });
-
- describe('when the mask state is invalid', () => {
- beforeEach(async () => {
- const [variable] = mockVariables;
- const invalidMaskVariable = {
- ...variable,
- value: 'd:;',
- masked: false,
- };
- createComponent({
- mountFn: mountExtended,
- props: { selectedVariable: invalidMaskVariable },
- });
- trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
- await findMaskedVariableCheckbox().trigger('click');
- });
-
- it('disables the submit button', () => {
- expect(findAddorUpdateButton().attributes('disabled')).toBeDefined();
- });
-
- it('shows the correct error text and help text', () => {
- expect(findModal().text()).toContain(maskError);
- expect(findModal().text()).toContain(helpText);
- });
-
- it('sends the correct tracking event', () => {
- expect(trackingSpy).toHaveBeenCalledWith(undefined, EVENT_ACTION, {
- label: EVENT_LABEL,
- property: ';',
- });
- });
- });
-
- describe.each`
- value | masked | eventSent | trackingErrorProperty
- ${'secretValue'} | ${false} | ${0} | ${null}
- ${'short'} | ${true} | ${0} | ${null}
- ${'dollar$ign'} | ${false} | ${1} | ${'$'}
- ${'dollar$ign'} | ${true} | ${1} | ${'$'}
- ${'unsupported|char'} | ${true} | ${1} | ${'|'}
- ${'unsupported|char'} | ${false} | ${0} | ${null}
- `('Adding a new variable', ({ value, masked, eventSent, trackingErrorProperty }) => {
- beforeEach(async () => {
- const [variable] = mockVariables;
- const invalidKeyVariable = {
- ...variable,
- value: '',
- masked: false,
- };
- createComponent({
- mountFn: mountExtended,
- props: { selectedVariable: invalidKeyVariable },
- });
- trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
- await findValueField().vm.$emit('input', value);
- if (masked) {
- await findMaskedVariableCheckbox().trigger('click');
- }
- });
-
- it(`${
- eventSent > 0 ? 'sends the correct' : 'does not send the'
- } variable validation tracking event with ${value}`, () => {
- expect(trackingSpy).toHaveBeenCalledTimes(eventSent);
-
- if (eventSent > 0) {
- expect(trackingSpy).toHaveBeenCalledWith(undefined, EVENT_ACTION, {
- label: EVENT_LABEL,
- property: trackingErrorProperty,
- });
- }
- });
- });
-
- describe('when masked variable has acceptable value', () => {
- beforeEach(() => {
- const [variable] = mockVariables;
- const validMaskandKeyVariable = {
- ...variable,
- key: AWS_ACCESS_KEY_ID,
- value: '12345678',
- masked: true,
- };
- createComponent({
- mountFn: mountExtended,
- props: { selectedVariable: validMaskandKeyVariable },
- });
- });
-
- it('shows the help text', () => {
- expect(findModal().text()).toContain(helpText);
- });
-
- it('does not disable the submit button', () => {
- expect(findAddorUpdateButton().attributes('disabled')).toBeUndefined();
- });
- });
- });
-});
diff --git a/spec/frontend/ci/ci_variable_list/components/ci_variable_settings_spec.js b/spec/frontend/ci/ci_variable_list/components/ci_variable_settings_spec.js
index 04145c2c6aa..01d3cdf504d 100644
--- a/spec/frontend/ci/ci_variable_list/components/ci_variable_settings_spec.js
+++ b/spec/frontend/ci/ci_variable_list/components/ci_variable_settings_spec.js
@@ -1,6 +1,5 @@
import { shallowMount } from '@vue/test-utils';
import CiVariableSettings from '~/ci/ci_variable_list/components/ci_variable_settings.vue';
-import CiVariableModal from '~/ci/ci_variable_list/components/ci_variable_modal.vue';
import CiVariableTable from '~/ci/ci_variable_list/components/ci_variable_table.vue';
import CiVariableDrawer from '~/ci/ci_variable_list/components/ci_variable_drawer.vue';
@@ -30,20 +29,13 @@ describe('Ci variable table', () => {
const findCiVariableDrawer = () => wrapper.findComponent(CiVariableDrawer);
const findCiVariableTable = () => wrapper.findComponent(CiVariableTable);
- const findCiVariableModal = () => wrapper.findComponent(CiVariableModal);
- const createComponent = ({ props = {}, featureFlags = {} } = {}) => {
+ const createComponent = ({ props = {} } = {}) => {
wrapper = shallowMount(CiVariableSettings, {
propsData: {
...defaultProps,
...props,
},
- provide: {
- glFeatures: {
- ciVariableDrawer: false,
- ...featureFlags,
- },
- },
});
};
@@ -60,24 +52,8 @@ describe('Ci variable table', () => {
});
});
- it('passes props down correctly to the ci modal', async () => {
- createComponent();
-
- await findCiVariableTable().vm.$emit('set-selected-variable');
-
- expect(findCiVariableModal().props()).toEqual({
- areEnvironmentsLoading: defaultProps.areEnvironmentsLoading,
- areScopedVariablesAvailable: defaultProps.areScopedVariablesAvailable,
- environments: defaultProps.environments,
- hideEnvironmentScope: defaultProps.hideEnvironmentScope,
- variables: defaultProps.variables,
- mode: ADD_VARIABLE_ACTION,
- selectedVariable: {},
- });
- });
-
it('passes props down correctly to the ci drawer', async () => {
- createComponent({ featureFlags: { ciVariableDrawer: true } });
+ createComponent();
await findCiVariableTable().vm.$emit('set-selected-variable');
@@ -92,55 +68,51 @@ describe('Ci variable table', () => {
});
});
- describe.each`
- bool | flagStatus | elementName | findElement
- ${false} | ${'disabled'} | ${'modal'} | ${findCiVariableModal}
- ${true} | ${'enabled'} | ${'drawer'} | ${findCiVariableDrawer}
- `('when ciVariableDrawer feature flag is $flagStatus', ({ bool, elementName, findElement }) => {
+ describe('drawer behavior', () => {
beforeEach(() => {
- createComponent({ featureFlags: { ciVariableDrawer: bool } });
+ createComponent();
});
- it(`${elementName} is hidden by default`, () => {
- expect(findElement().exists()).toBe(false);
+ it(`drawer is hidden by default`, () => {
+ expect(findCiVariableDrawer().exists()).toBe(false);
});
- it(`shows ${elementName} when adding a new variable`, async () => {
+ it(`shows drawer when adding a new variable`, async () => {
await findCiVariableTable().vm.$emit('set-selected-variable');
- expect(findElement().exists()).toBe(true);
+ expect(findCiVariableDrawer().exists()).toBe(true);
});
- it(`shows ${elementName} when updating a variable`, async () => {
+ it(`shows drawer when updating a variable`, async () => {
await findCiVariableTable().vm.$emit('set-selected-variable', newVariable);
- expect(findElement().exists()).toBe(true);
+ expect(findCiVariableDrawer().exists()).toBe(true);
});
- it(`hides ${elementName} when closing the form`, async () => {
+ it(`hides drawer when closing the form`, async () => {
await findCiVariableTable().vm.$emit('set-selected-variable');
- expect(findElement().isVisible()).toBe(true);
+ expect(findCiVariableDrawer().isVisible()).toBe(true);
- await findElement().vm.$emit('close-form');
+ await findCiVariableDrawer().vm.$emit('close-form');
- expect(findElement().exists()).toBe(false);
+ expect(findCiVariableDrawer().exists()).toBe(false);
});
- it(`passes down ADD mode to ${elementName} when receiving an empty variable`, async () => {
+ it(`passes down ADD mode to drawer when receiving an empty variable`, async () => {
await findCiVariableTable().vm.$emit('set-selected-variable');
- expect(findElement().props('mode')).toBe(ADD_VARIABLE_ACTION);
+ expect(findCiVariableDrawer().props('mode')).toBe(ADD_VARIABLE_ACTION);
});
- it(`passes down EDIT mode to ${elementName} when receiving a variable`, async () => {
+ it(`passes down EDIT mode to drawer when receiving a variable`, async () => {
await findCiVariableTable().vm.$emit('set-selected-variable', newVariable);
- expect(findElement().props('mode')).toBe(EDIT_VARIABLE_ACTION);
+ expect(findCiVariableDrawer().props('mode')).toBe(EDIT_VARIABLE_ACTION);
});
});
- describe('variable events for modal', () => {
+ describe('variable events', () => {
beforeEach(() => {
createComponent();
});
@@ -153,25 +125,6 @@ describe('Ci variable table', () => {
`('bubbles up the $eventName event', async ({ eventName }) => {
await findCiVariableTable().vm.$emit('set-selected-variable');
- await findCiVariableModal().vm.$emit(eventName, newVariable);
-
- expect(wrapper.emitted(eventName)).toEqual([[newVariable]]);
- });
- });
-
- describe('variable events for drawer', () => {
- beforeEach(() => {
- createComponent({ featureFlags: { ciVariableDrawer: true } });
- });
-
- it.each`
- eventName
- ${'add-variable'}
- ${'update-variable'}
- ${'delete-variable'}
- `('bubbles up the $eventName event', async ({ eventName }) => {
- await findCiVariableTable().vm.$emit('set-selected-variable');
-
await findCiVariableDrawer().vm.$emit(eventName, newVariable);
expect(wrapper.emitted(eventName)).toEqual([[newVariable]]);
@@ -195,7 +148,7 @@ describe('Ci variable table', () => {
});
});
- describe('environment events for modal', () => {
+ describe('environment events', () => {
beforeEach(() => {
createComponent();
});
@@ -203,20 +156,6 @@ describe('Ci variable table', () => {
it('bubbles up the search event', async () => {
await findCiVariableTable().vm.$emit('set-selected-variable');
- await findCiVariableModal().vm.$emit('search-environment-scope', 'staging');
-
- expect(wrapper.emitted('search-environment-scope')).toEqual([['staging']]);
- });
- });
-
- describe('environment events for drawer', () => {
- beforeEach(() => {
- createComponent({ featureFlags: { ciVariableDrawer: true } });
- });
-
- it('bubbles up the search event', async () => {
- await findCiVariableTable().vm.$emit('set-selected-variable');
-
await findCiVariableDrawer().vm.$emit('search-environment-scope', 'staging');
expect(wrapper.emitted('search-environment-scope')).toEqual([['staging']]);
diff --git a/spec/frontend/ci/common/pipelines_table_spec.js b/spec/frontend/ci/common/pipelines_table_spec.js
index 6cf391d72ca..f6d3121109f 100644
--- a/spec/frontend/ci/common/pipelines_table_spec.js
+++ b/spec/frontend/ci/common/pipelines_table_spec.js
@@ -16,7 +16,7 @@ import {
TRACKING_CATEGORIES,
} from '~/ci/constants';
-import CiBadgeLink from '~/vue_shared/components/ci_badge_link.vue';
+import CiIcon from '~/vue_shared/components/ci_icon.vue';
describe('Pipelines Table', () => {
let wrapper;
@@ -58,7 +58,7 @@ describe('Pipelines Table', () => {
};
const findGlTableLite = () => wrapper.findComponent(GlTableLite);
- const findCiBadgeLink = () => wrapper.findComponent(CiBadgeLink);
+ const findCiIcon = () => wrapper.findComponent(CiIcon);
const findPipelineInfo = () => wrapper.findComponent(PipelineUrl);
const findTriggerer = () => wrapper.findComponent(PipelineTriggerer);
const findLegacyPipelineMiniGraph = () => wrapper.findComponent(LegacyPipelineMiniGraph);
@@ -96,7 +96,7 @@ describe('Pipelines Table', () => {
describe('status cell', () => {
it('should render a status badge', () => {
- expect(findCiBadgeLink().exists()).toBe(true);
+ expect(findCiIcon().exists()).toBe(true);
});
});
@@ -265,7 +265,7 @@ describe('Pipelines Table', () => {
});
it('tracks status badge click', () => {
- findCiBadgeLink().vm.$emit('ciStatusBadgeClick');
+ findCiIcon().vm.$emit('ciStatusBadgeClick');
expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_ci_status_badge', {
label: TRACKING_CATEGORIES.table,
diff --git a/spec/frontend/ci/job_details/components/job_header_spec.js b/spec/frontend/ci/job_details/components/job_header_spec.js
index 609369316f5..d12267807ac 100644
--- a/spec/frontend/ci/job_details/components/job_header_spec.js
+++ b/spec/frontend/ci/job_details/components/job_header_spec.js
@@ -1,7 +1,7 @@
import { GlButton, GlAvatarLink, GlTooltip } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
-import CiBadgeLink from '~/vue_shared/components/ci_badge_link.vue';
+import CiIcon from '~/vue_shared/components/ci_icon.vue';
import JobHeader from '~/ci/job_details/components/job_header.vue';
import TimeagoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
@@ -29,7 +29,7 @@ describe('Header CI Component', () => {
shouldRenderTriggeredLabel: true,
};
- const findCiBadgeLink = () => wrapper.findComponent(CiBadgeLink);
+ const findCiIcon = () => wrapper.findComponent(CiIcon);
const findTimeAgo = () => wrapper.findComponent(TimeagoTooltip);
const findUserLink = () => wrapper.findComponent(GlAvatarLink);
const findSidebarToggleBtn = () => wrapper.findComponent(GlButton);
@@ -57,7 +57,7 @@ describe('Header CI Component', () => {
});
it('should render status badge', () => {
- expect(findCiBadgeLink().exists()).toBe(true);
+ expect(findCiIcon().exists()).toBe(true);
});
it('should render timeago date', () => {
diff --git a/spec/frontend/ci/job_details/components/log/line_header_spec.js b/spec/frontend/ci/job_details/components/log/line_header_spec.js
index 45296e4b6c2..c75f5fa30d5 100644
--- a/spec/frontend/ci/job_details/components/log/line_header_spec.js
+++ b/spec/frontend/ci/job_details/components/log/line_header_spec.js
@@ -1,3 +1,4 @@
+import { GlIcon } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import { nextTick } from 'vue';
import setWindowLocation from 'helpers/set_window_location_helper';
@@ -30,6 +31,8 @@ describe('Job Log Header Line', () => {
});
};
+ const findIcon = () => wrapper.findComponent(GlIcon);
+
describe('line', () => {
beforeEach(() => {
createComponent();
@@ -48,23 +51,33 @@ describe('Job Log Header Line', () => {
});
});
- describe('when isCloses is true', () => {
+ describe('when isClosed is true', () => {
beforeEach(() => {
createComponent({ ...defaultProps, isClosed: true });
});
it('sets icon name to be chevron-lg-right', () => {
- expect(wrapper.vm.iconName).toEqual('chevron-lg-right');
+ expect(findIcon().props('name')).toEqual('chevron-lg-right');
});
});
- describe('when isCloses is false', () => {
+ describe('when isClosed is false', () => {
beforeEach(() => {
createComponent({ ...defaultProps, isClosed: false });
});
it('sets icon name to be chevron-lg-down', () => {
- expect(wrapper.vm.iconName).toEqual('chevron-lg-down');
+ expect(findIcon().props('name')).toEqual('chevron-lg-down');
+ });
+ });
+
+ describe('when isClosed is not defined', () => {
+ beforeEach(() => {
+ createComponent({ ...defaultProps, isClosed: undefined });
+ });
+
+ it('sets icon name to be chevron-lg-right', () => {
+ expect(findIcon().props('name')).toEqual('chevron-lg-down');
});
});
diff --git a/spec/frontend/ci/job_details/components/log/mock_data.js b/spec/frontend/ci/job_details/components/log/mock_data.js
index 14669872cc1..d9b1354f475 100644
--- a/spec/frontend/ci/job_details/components/log/mock_data.js
+++ b/spec/frontend/ci/job_details/components/log/mock_data.js
@@ -1,67 +1,73 @@
-export const mockJobLog = [
+export const mockJobLines = [
{
- offset: 1000,
- content: [{ text: 'Running with gitlab-runner 12.1.0 (de7731dd)' }],
+ offset: 0,
+ content: [
+ {
+ text: 'Running with gitlab-runner 12.1.0 (de7731dd)',
+ style: 'term-fg-l-cyan term-bold',
+ },
+ ],
},
{
offset: 1001,
content: [{ text: ' on docker-auto-scale-com 8a6210b8' }],
},
+];
+
+export const mockEmptySection = [
{
offset: 1002,
content: [
{
- text: 'Using Docker executor with image dev.gitlab.org3',
+ text: 'Resolving secrets',
+ style: 'term-fg-l-cyan term-bold',
},
],
- section: 'prepare-executor',
+ section: 'resolve-secrets',
section_header: true,
},
{
offset: 1003,
- content: [{ text: 'Docker executor with image registry.gitlab.com ...' }],
- section: 'prepare-executor',
- },
- {
- offset: 1004,
- content: [{ text: 'Starting service ...', style: 'term-fg-l-green' }],
- section: 'prepare-executor',
- },
- {
- offset: 1005,
content: [],
- section: 'prepare-executor',
- section_duration: '00:09',
+ section: 'resolve-secrets',
+ section_footer: true,
+ section_duration: '00:00',
},
+];
+
+export const mockContentSection = [
{
- offset: 1006,
+ offset: 1004,
content: [
{
- text: 'Getting source from Git repository',
+ text: 'Using Docker executor with image dev.gitlab.org3',
},
],
- section: 'get-sources',
+ section: 'prepare-executor',
section_header: true,
},
{
- offset: 1007,
- content: [{ text: 'Fetching changes with git depth set to 20...' }],
- section: 'get-sources',
+ offset: 1005,
+ content: [{ text: 'Docker executor with image registry.gitlab.com ...' }],
+ section: 'prepare-executor',
},
{
- offset: 1008,
- content: [{ text: 'Initialized empty Git repository', style: 'term-fg-l-green' }],
- section: 'get-sources',
+ offset: 1006,
+ content: [{ text: 'Starting service ...', style: 'term-fg-l-green' }],
+ section: 'prepare-executor',
},
{
- offset: 1009,
+ offset: 1007,
content: [],
- section: 'get-sources',
- section_duration: '00:19',
+ section: 'prepare-executor',
+ section_footer: true,
+ section_duration: '00:09',
},
];
-export const mockJobLogLineCount = 8; // `text` entries in mockJobLog
+export const mockJobLog = [...mockJobLines, ...mockEmptySection, ...mockContentSection];
+
+export const mockJobLogLineCount = 6; // `text` entries in mockJobLog
export const originalTrace = [
{
diff --git a/spec/frontend/ci/job_details/components/sidebar/stages_dropdown_spec.js b/spec/frontend/ci/job_details/components/sidebar/stages_dropdown_spec.js
index e007896c81e..54c5a73f757 100644
--- a/spec/frontend/ci/job_details/components/sidebar/stages_dropdown_spec.js
+++ b/spec/frontend/ci/job_details/components/sidebar/stages_dropdown_spec.js
@@ -3,7 +3,7 @@ import { shallowMount } from '@vue/test-utils';
import { Mousetrap } from '~/lib/mousetrap';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import StagesDropdown from '~/ci/job_details/components/sidebar/stages_dropdown.vue';
-import CiBadgeLink from '~/vue_shared/components/ci_badge_link.vue';
+import CiIcon from '~/vue_shared/components/ci_icon.vue';
import * as copyToClipboard from '~/behaviors/copy_to_clipboard';
import {
mockPipelineWithoutRef,
@@ -15,7 +15,7 @@ import {
describe('Stages Dropdown', () => {
let wrapper;
- const findStatus = () => wrapper.findComponent(CiBadgeLink);
+ const findStatus = () => wrapper.findComponent(CiIcon);
const findDropdown = () => wrapper.findComponent(GlDisclosureDropdown);
const findSelectedStageText = () => findDropdown().props('toggleText');
@@ -47,7 +47,6 @@ describe('Stages Dropdown', () => {
it('renders pipeline status', () => {
expect(findStatus().props('status')).toBe(mockPipelineWithoutMR.details.status);
- expect(findStatus().props('size')).toBe('sm');
});
it('renders dropdown with stages', () => {
diff --git a/spec/frontend/ci/job_details/job_app_spec.js b/spec/frontend/ci/job_details/job_app_spec.js
index ff84b2d0283..2bd0429ef56 100644
--- a/spec/frontend/ci/job_details/job_app_spec.js
+++ b/spec/frontend/ci/job_details/job_app_spec.js
@@ -311,6 +311,8 @@ describe('Job App', () => {
it('should render job log', () => {
expect(findJobLog().exists()).toBe(true);
+
+ expect(findJobLog().props()).toEqual({ searchResults: [] });
});
});
diff --git a/spec/frontend/ci/job_details/store/actions_spec.js b/spec/frontend/ci/job_details/store/actions_spec.js
index 2799bc9578c..849f55ac444 100644
--- a/spec/frontend/ci/job_details/store/actions_spec.js
+++ b/spec/frontend/ci/job_details/store/actions_spec.js
@@ -284,7 +284,7 @@ describe('Job State actions', () => {
});
});
- describe('error', () => {
+ describe('server error', () => {
beforeEach(() => {
mock.onGet(`${TEST_HOST}/endpoint/trace.json`).reply(HTTP_STATUS_INTERNAL_SERVER_ERROR);
});
@@ -303,6 +303,28 @@ describe('Job State actions', () => {
);
});
});
+
+ describe('unexpected error', () => {
+ beforeEach(() => {
+ mock.onGet(`${TEST_HOST}/endpoint/trace.json`).reply(() => {
+ throw new Error('an error');
+ });
+ });
+
+ it('dispatches requestJobLog and receiveJobLogError', () => {
+ return testAction(
+ fetchJobLog,
+ null,
+ mockedState,
+ [],
+ [
+ {
+ type: 'receiveJobLogError',
+ },
+ ],
+ );
+ });
+ });
});
describe('startPollingJobLog', () => {
diff --git a/spec/frontend/ci/job_details/store/mutations_spec.js b/spec/frontend/ci/job_details/store/mutations_spec.js
index 78b29efed68..601dff47584 100644
--- a/spec/frontend/ci/job_details/store/mutations_spec.js
+++ b/spec/frontend/ci/job_details/store/mutations_spec.js
@@ -1,6 +1,7 @@
import * as types from '~/ci/job_details/store/mutation_types';
import mutations from '~/ci/job_details/store/mutations';
import state from '~/ci/job_details/store/state';
+import * as utils from '~/ci/job_details/store/utils';
describe('Jobs Store Mutations', () => {
let stateCopy;
@@ -87,50 +88,91 @@ describe('Jobs Store Mutations', () => {
});
describe('with new job log', () => {
+ const mockLog = {
+ append: false,
+ size: 511846,
+ complete: true,
+ lines: [
+ {
+ offset: 1,
+ content: [{ text: 'Line content' }],
+ },
+ ],
+ };
+
+ beforeEach(() => {
+ jest.spyOn(utils, 'logLinesParser');
+ });
+
+ afterEach(() => {
+ utils.logLinesParser.mockRestore();
+ });
+
describe('log.lines', () => {
- describe('when append is true', () => {
+ describe('when it is defined', () => {
it('sets the parsed log', () => {
- mutations[types.RECEIVE_JOB_LOG_SUCCESS](stateCopy, {
- append: true,
- size: 511846,
- complete: true,
- lines: [
- {
- offset: 1,
- content: [{ text: 'Running with gitlab-runner 11.12.1 (5a147c92)' }],
- },
- ],
- });
+ mutations[types.RECEIVE_JOB_LOG_SUCCESS](stateCopy, mockLog);
+
+ expect(utils.logLinesParser).toHaveBeenCalledWith(mockLog.lines, [], '');
expect(stateCopy.jobLog).toEqual([
{
offset: 1,
- content: [{ text: 'Running with gitlab-runner 11.12.1 (5a147c92)' }],
+ content: [{ text: 'Line content' }],
lineNumber: 1,
},
]);
});
});
- describe('when it is defined', () => {
+ describe('when it is defined and location.hash is set', () => {
+ beforeEach(() => {
+ window.location.hash = '#L1';
+ });
+
it('sets the parsed log', () => {
- mutations[types.RECEIVE_JOB_LOG_SUCCESS](stateCopy, {
- append: false,
- size: 511846,
- complete: true,
- lines: [
- { offset: 0, content: [{ text: 'Running with gitlab-runner 11.11.1 (5a147c92)' }] },
- ],
- });
+ mutations[types.RECEIVE_JOB_LOG_SUCCESS](stateCopy, mockLog);
+
+ expect(utils.logLinesParser).toHaveBeenCalledWith(mockLog.lines, [], '#L1');
expect(stateCopy.jobLog).toEqual([
{
- offset: 0,
- content: [{ text: 'Running with gitlab-runner 11.11.1 (5a147c92)' }],
+ offset: 1,
+ content: [{ text: 'Line content' }],
lineNumber: 1,
},
]);
});
+
+ describe('when append is true', () => {
+ it('sets the parsed log', () => {
+ stateCopy.jobLog = [
+ {
+ offset: 0,
+ content: [{ text: 'Previous line content' }],
+ lineNumber: 1,
+ },
+ ];
+
+ mutations[types.RECEIVE_JOB_LOG_SUCCESS](stateCopy, {
+ ...mockLog,
+ append: true,
+ });
+
+ expect(stateCopy.jobLog).toEqual([
+ {
+ offset: 0,
+ content: [{ text: 'Previous line content' }],
+ lineNumber: 1,
+ },
+ {
+ offset: 1,
+ content: [{ text: 'Line content' }],
+ lineNumber: 2,
+ },
+ ]);
+ });
+ });
});
describe('when it is null', () => {
diff --git a/spec/frontend/ci/job_details/store/utils_spec.js b/spec/frontend/ci/job_details/store/utils_spec.js
index 394ce0ab737..8fc4eeb0ca8 100644
--- a/spec/frontend/ci/job_details/store/utils_spec.js
+++ b/spec/frontend/ci/job_details/store/utils_spec.js
@@ -195,11 +195,9 @@ describe('Jobs Store Utils', () => {
expect(result[0].lineNumber).toEqual(1);
expect(result[1].lineNumber).toEqual(2);
expect(result[2].line.lineNumber).toEqual(3);
- expect(result[2].lines[0].lineNumber).toEqual(4);
- expect(result[2].lines[1].lineNumber).toEqual(5);
- expect(result[3].line.lineNumber).toEqual(6);
- expect(result[3].lines[0].lineNumber).toEqual(7);
- expect(result[3].lines[1].lineNumber).toEqual(8);
+ expect(result[3].line.lineNumber).toEqual(4);
+ expect(result[3].lines[0].lineNumber).toEqual(5);
+ expect(result[3].lines[1].lineNumber).toEqual(6);
});
});
@@ -215,16 +213,16 @@ describe('Jobs Store Utils', () => {
});
it('creates a lines array property with the content of the collapsible section', () => {
- expect(result[2].lines.length).toEqual(2);
- expect(result[2].lines[0].content).toEqual(mockJobLog[3].content);
- expect(result[2].lines[1].content).toEqual(mockJobLog[4].content);
+ expect(result[3].lines.length).toEqual(2);
+ expect(result[3].lines[0].content).toEqual(mockJobLog[5].content);
+ expect(result[3].lines[1].content).toEqual(mockJobLog[6].content);
});
});
describe('section duration', () => {
it('adds the section information to the header section', () => {
- expect(result[2].line.section_duration).toEqual(mockJobLog[5].section_duration);
- expect(result[3].line.section_duration).toEqual(mockJobLog[9].section_duration);
+ expect(result[2].line.section_duration).toEqual(mockJobLog[3].section_duration);
+ expect(result[3].line.section_duration).toEqual(mockJobLog[7].section_duration);
});
it('does not add section duration as a line', () => {
diff --git a/spec/frontend/ci/jobs_page/components/job_cells/job_cell_spec.js b/spec/frontend/ci/jobs_page/components/job_cells/job_cell_spec.js
index bb44d970bd7..68ff2403b30 100644
--- a/spec/frontend/ci/jobs_page/components/job_cells/job_cell_spec.js
+++ b/spec/frontend/ci/jobs_page/components/job_cells/job_cell_spec.js
@@ -107,11 +107,11 @@ describe('Job Cell', () => {
});
it.each`
- testId | text
- ${'manual-job-badge'} | ${'manual'}
- ${'triggered-job-badge'} | ${'triggered'}
- ${'fail-job-badge'} | ${'allowed to fail'}
- ${'delayed-job-badge'} | ${'delayed'}
+ testId | text
+ ${'manual-job-badge'} | ${'manual'}
+ ${'trigger-token-job-badge'} | ${'trigger token'}
+ ${'fail-job-badge'} | ${'allowed to fail'}
+ ${'delayed-job-badge'} | ${'delayed'}
`('displays the static $text badge', ({ testId, text }) => {
createComponent({
manualJob: true,
diff --git a/spec/frontend/ci/jobs_page/components/jobs_table_spec.js b/spec/frontend/ci/jobs_page/components/jobs_table_spec.js
index d4e0ce92bc2..d14afe7dd3e 100644
--- a/spec/frontend/ci/jobs_page/components/jobs_table_spec.js
+++ b/spec/frontend/ci/jobs_page/components/jobs_table_spec.js
@@ -3,7 +3,7 @@ import { mount } from '@vue/test-utils';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import JobsTable from '~/ci/jobs_page/components/jobs_table.vue';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
-import CiBadgeLink from '~/vue_shared/components/ci_badge_link.vue';
+import CiIcon from '~/vue_shared/components/ci_icon.vue';
import { DEFAULT_FIELDS_ADMIN } from '~/ci/admin/jobs_table/constants';
import ProjectCell from '~/ci/admin/jobs_table/components/cells/project_cell.vue';
import RunnerCell from '~/ci/admin/jobs_table/components/cells/runner_cell.vue';
@@ -13,7 +13,7 @@ describe('Jobs Table', () => {
let wrapper;
const findTable = () => wrapper.findComponent(GlTable);
- const findCiBadgeLink = () => wrapper.findComponent(CiBadgeLink);
+ const findCiIcon = () => wrapper.findComponent(CiIcon);
const findTableRows = () => wrapper.findAllByTestId('jobs-table-row');
const findJobStage = () => wrapper.findByTestId('job-stage-name');
const findJobName = () => wrapper.findByTestId('job-name');
@@ -45,7 +45,7 @@ describe('Jobs Table', () => {
});
it('displays job status', () => {
- expect(findCiBadgeLink().exists()).toBe(true);
+ expect(findCiIcon().exists()).toBe(true);
});
it('displays the job stage, id and name', () => {
diff --git a/spec/frontend/ci/pipeline_details/graph/components/graph_component_spec.js b/spec/frontend/ci/pipeline_details/graph/components/graph_component_spec.js
index a98e79c69fe..c3f22749978 100644
--- a/spec/frontend/ci/pipeline_details/graph/components/graph_component_spec.js
+++ b/spec/frontend/ci/pipeline_details/graph/components/graph_component_spec.js
@@ -19,6 +19,10 @@ describe('graph component', () => {
const findLinksLayer = () => wrapper.findComponent(LinksLayer);
const findStageColumns = () => wrapper.findAllComponents(StageColumnComponent);
const findStageNameInJob = () => wrapper.findByTestId('stage-name-in-job');
+ const findPipelineContainer = () => wrapper.findByTestId('pipeline-container');
+ const findRootGraphLayout = () => wrapper.findByTestId('stage-column');
+ const findStageColumnTitle = () => wrapper.findByTestId('stage-column-title');
+ const findJobItem = () => wrapper.findComponent(JobItem);
const defaultProps = {
pipeline: generateResponse(mockPipelineResponse, 'root/fungi-xoxo'),
@@ -42,6 +46,9 @@ describe('graph component', () => {
mountFn = shallowMount,
props = {},
stubOverride = {},
+ glFeatures = {
+ newPipelineGraph: false,
+ },
} = {}) => {
wrapper = mountFn(PipelineGraph, {
propsData: {
@@ -61,6 +68,9 @@ describe('graph component', () => {
'job-group-dropdown': true,
...stubOverride,
},
+ provide: {
+ glFeatures,
+ },
});
};
@@ -112,9 +122,8 @@ describe('graph component', () => {
});
it('dims unrelated jobs', () => {
- const unrelatedJob = wrapper.findComponent(JobItem);
expect(findLinksLayer().emitted().highlightedJobsChange).toHaveLength(1);
- expect(unrelatedJob.classes('gl-opacity-3')).toBe(true);
+ expect(findJobItem().classes('gl-opacity-3')).toBe(true);
});
});
});
@@ -179,4 +188,82 @@ describe('graph component', () => {
expect(findDownstreamColumn().props().linkedPipelines).toHaveLength(1);
});
});
+
+ describe.each`
+ name | value | state
+ ${'disabled'} | ${false} | ${'should not'}
+ ${'enabled'} | ${true} | ${'should'}
+ `('With feature flag newPipelineGraph $name', ({ value, state }) => {
+ beforeEach(() => {
+ createComponent({
+ mountFn: mountExtended,
+ stubOverride: { 'job-item': false, StageColumnComponent },
+ glFeatures: {
+ newPipelineGraph: value,
+ },
+ stubs: {
+ StageColumnComponent,
+ },
+ });
+ });
+
+ it(`${state} add class pipeline-graph-container on wrapper`, () => {
+ expect(findPipelineContainer().classes('pipeline-graph-container')).toBe(value);
+ });
+
+ it(`${state} add class is-stage-view on rootGraphLayout`, () => {
+ expect(findRootGraphLayout().classes('is-stage-view')).toBe(value);
+ });
+
+ it(`${state} add titleClasses on stageColumnTitle`, () => {
+ const titleClasses = [
+ 'gl-font-weight-bold',
+ 'gl-pipeline-job-width',
+ 'gl-text-truncate',
+ 'gl-line-height-36',
+ 'gl-pl-4',
+ 'gl-mb-n2',
+ ];
+ const legacyTitleClasses = [
+ 'gl-font-weight-bold',
+ 'gl-pipeline-job-width',
+ 'gl-text-truncate',
+ 'gl-line-height-36',
+ 'gl-pl-3',
+ ];
+ const checkClasses = value ? titleClasses : legacyTitleClasses;
+
+ expect(findStageColumnTitle().classes()).toEqual(expect.arrayContaining(checkClasses));
+ });
+
+ it(`${state} add jobClasses on findJobItem`, () => {
+ const jobClasses = [
+ 'gl-p-3',
+ 'gl-border-0',
+ 'gl-bg-transparent',
+ 'gl-rounded-base',
+ 'gl-hover-bg-gray-50',
+ 'gl-focus-bg-gray-50',
+ 'gl-hover-text-gray-900',
+ 'gl-focus-text-gray-900',
+ ];
+ const legacyJobClasses = [
+ 'gl-p-3',
+ 'gl-border-gray-100',
+ 'gl-border-solid',
+ 'gl-border-1',
+ 'gl-bg-white',
+ 'gl-rounded-7',
+ 'gl-hover-bg-gray-50',
+ 'gl-focus-bg-gray-50',
+ 'gl-hover-text-gray-900',
+ 'gl-focus-text-gray-900',
+ 'gl-hover-border-gray-200',
+ 'gl-focus-border-gray-200',
+ ];
+ const checkClasses = value ? jobClasses : legacyJobClasses;
+
+ expect(findJobItem().props('cssClassJobName')).toEqual(expect.arrayContaining(checkClasses));
+ });
+ });
});
diff --git a/spec/frontend/ci/pipeline_details/graph/components/job_item_spec.js b/spec/frontend/ci/pipeline_details/graph/components/job_item_spec.js
index de9ee8a16bf..10db7f398fe 100644
--- a/spec/frontend/ci/pipeline_details/graph/components/job_item_spec.js
+++ b/spec/frontend/ci/pipeline_details/graph/components/job_item_spec.js
@@ -5,7 +5,7 @@ import JobItem from '~/ci/pipeline_details/graph/components/job_item.vue';
import axios from '~/lib/utils/axios_utils';
import { useLocalStorageSpy } from 'helpers/local_storage_helper';
import ActionComponent from '~/ci/common/private/job_action_component.vue';
-import CiBadgeLink from '~/vue_shared/components/ci_badge_link.vue';
+import CiIcon from '~/vue_shared/components/ci_icon.vue';
import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper';
import {
@@ -31,7 +31,7 @@ describe('pipeline graph job item', () => {
const findActionComponent = () => wrapper.findByTestId('ci-action-button');
const findBadge = () => wrapper.findByTestId('job-bridge-badge');
const findJobLink = () => wrapper.findByTestId('job-with-link');
- const findJobCiBadge = () => wrapper.findComponent(CiBadgeLink);
+ const findJobCiIcon = () => wrapper.findComponent(CiIcon);
const findModal = () => wrapper.findComponent(GlModal);
const clickOnModalPrimaryBtn = () => findModal().vm.$emit('primary');
@@ -60,7 +60,7 @@ describe('pipeline graph job item', () => {
...mocks,
},
stubs: {
- CiBadgeLink,
+ CiIcon,
},
});
};
@@ -86,8 +86,10 @@ describe('pipeline graph job item', () => {
expect(link.attributes('title')).toBe(`${mockJob.name} - ${mockJob.status.label}`);
- expect(findJobCiBadge().exists()).toBe(true);
- expect(findJobCiBadge().find('.ci-status-icon-success').exists()).toBe(true);
+ expect(findJobCiIcon().exists()).toBe(true);
+ expect(findJobCiIcon().find('[data-testid="status_success_borderless-icon"]').exists()).toBe(
+ true,
+ );
expect(wrapper.text()).toBe(mockJob.name);
});
@@ -105,8 +107,10 @@ describe('pipeline graph job item', () => {
});
it('should render status and name', () => {
- expect(findJobCiBadge().exists()).toBe(true);
- expect(findJobCiBadge().find('.ci-status-icon-success').exists()).toBe(true);
+ expect(findJobCiIcon().exists()).toBe(true);
+ expect(findJobCiIcon().find('[data-testid="status_success_borderless-icon"]').exists()).toBe(
+ true,
+ );
expect(findJobLink().exists()).toBe(false);
expect(wrapper.text()).toBe(mockJobWithoutDetails.name);
@@ -117,12 +121,12 @@ describe('pipeline graph job item', () => {
});
});
- describe('CiBadgeLink', () => {
+ describe('CiIcon', () => {
it('should not render a link', () => {
createWrapper();
- expect(findJobCiBadge().exists()).toBe(true);
- expect(findJobCiBadge().props('useLink')).toBe(false);
+ expect(findJobCiIcon().exists()).toBe(true);
+ expect(findJobCiIcon().props('useLink')).toBe(false);
});
});
diff --git a/spec/frontend/ci/pipeline_details/graph/components/job_name_component_spec.js b/spec/frontend/ci/pipeline_details/graph/components/job_name_component_spec.js
index ca201aee648..1da85ad9f78 100644
--- a/spec/frontend/ci/pipeline_details/graph/components/job_name_component_spec.js
+++ b/spec/frontend/ci/pipeline_details/graph/components/job_name_component_spec.js
@@ -25,6 +25,6 @@ describe('job name component', () => {
it('should render an icon with the provided status', () => {
expect(wrapper.findComponent(CiIcon).exists()).toBe(true);
- expect(wrapper.find('.ci-status-icon-success').exists()).toBe(true);
+ expect(wrapper.find('[data-testid="status_success_borderless-icon"]').exists()).toBe(true);
});
});
diff --git a/spec/frontend/ci/pipeline_details/graph/components/linked_pipeline_spec.js b/spec/frontend/ci/pipeline_details/graph/components/linked_pipeline_spec.js
index 5fe8581e81b..72be51575d7 100644
--- a/spec/frontend/ci/pipeline_details/graph/components/linked_pipeline_spec.js
+++ b/spec/frontend/ci/pipeline_details/graph/components/linked_pipeline_spec.js
@@ -93,7 +93,7 @@ describe('Linked pipeline', () => {
});
it('should render the pipeline status icon svg', () => {
- expect(wrapper.find('.ci-status-icon-success svg').exists()).toBe(true);
+ expect(wrapper.findByTestId('status_success_borderless-icon').exists()).toBe(true);
});
it('should have a ci-status child component', () => {
diff --git a/spec/frontend/ci/pipeline_details/header/pipeline_details_header_spec.js b/spec/frontend/ci/pipeline_details/header/pipeline_details_header_spec.js
index 6e13658a773..e8e178ed148 100644
--- a/spec/frontend/ci/pipeline_details/header/pipeline_details_header_spec.js
+++ b/spec/frontend/ci/pipeline_details/header/pipeline_details_header_spec.js
@@ -7,7 +7,7 @@ import waitForPromises from 'helpers/wait_for_promises';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import PipelineDetailsHeader from '~/ci/pipeline_details/header/pipeline_details_header.vue';
import { BUTTON_TOOLTIP_RETRY, BUTTON_TOOLTIP_CANCEL } from '~/ci/constants';
-import CiBadgeLink from '~/vue_shared/components/ci_badge_link.vue';
+import CiIcon from '~/vue_shared/components/ci_icon.vue';
import cancelPipelineMutation from '~/ci/pipeline_details/graphql/mutations/cancel_pipeline.mutation.graphql';
import deletePipelineMutation from '~/ci/pipeline_details/graphql/mutations/delete_pipeline.mutation.graphql';
import retryPipelineMutation from '~/ci/pipeline_details/graphql/mutations/retry_pipeline.mutation.graphql';
@@ -56,7 +56,7 @@ describe('Pipeline details header', () => {
.mockResolvedValue(pipelineDeleteMutationResponseFailed);
const findAlert = () => wrapper.findComponent(GlAlert);
- const findStatus = () => wrapper.findComponent(CiBadgeLink);
+ const findStatus = () => wrapper.findComponent(CiIcon);
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const findAllBadges = () => wrapper.findAllComponents(GlBadge);
const findDeleteModal = () => wrapper.findComponent(GlModal);
@@ -94,9 +94,11 @@ describe('Pipeline details header', () => {
failureReason: 'pipeline failed',
badges: {
schedule: true,
+ trigger: false,
child: false,
latest: true,
mergeTrainPipeline: false,
+ mergedResultsPipeline: false,
invalid: false,
failed: false,
autoDevops: false,
@@ -178,6 +180,7 @@ describe('Pipeline details header', () => {
expect(findAllBadges()).toHaveLength(2);
expect(wrapper.findByText('latest').exists()).toBe(true);
expect(wrapper.findByText('Scheduled').exists()).toBe(true);
+ expect(wrapper.findByText('trigger token').exists()).toBe(false);
});
it('displays ref text', () => {
@@ -202,6 +205,21 @@ describe('Pipeline details header', () => {
});
});
+ describe('with triggered pipeline', () => {
+ beforeEach(async () => {
+ createComponent(defaultHandlers, {
+ ...defaultProps,
+ badges: { ...defaultProps.badges, trigger: true },
+ });
+
+ await waitForPromises();
+ });
+
+ it('displays triggered badge', () => {
+ expect(wrapper.findByText('trigger token').exists()).toBe(true);
+ });
+ });
+
describe('without pipeline name', () => {
it('displays commit title', async () => {
createComponent(defaultHandlers, { ...defaultProps, name: '' });
diff --git a/spec/frontend/ci/pipeline_editor/components/file-nav/branch_switcher_spec.js b/spec/frontend/ci/pipeline_editor/components/file-nav/branch_switcher_spec.js
index 4057759b9b9..d38226fedb2 100644
--- a/spec/frontend/ci/pipeline_editor/components/file-nav/branch_switcher_spec.js
+++ b/spec/frontend/ci/pipeline_editor/components/file-nav/branch_switcher_spec.js
@@ -1,12 +1,6 @@
import Vue from 'vue';
import VueApollo from 'vue-apollo';
-import {
- GlDropdown,
- GlDropdownItem,
- GlInfiniteScroll,
- GlLoadingIcon,
- GlSearchBoxByType,
-} from '@gitlab/ui';
+import { GlCollapsibleListbox, GlListboxItem } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
@@ -76,17 +70,15 @@ describe('Pipeline editor branch switcher', () => {
totalBranches: mockTotalBranches,
},
apolloProvider: mockApollo,
+ stubs: { GlCollapsibleListbox },
});
return waitForPromises();
};
- const findDropdown = () => wrapper.findComponent(GlDropdown);
- const findDropdownItems = () => wrapper.findAllComponents(GlDropdownItem);
- const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
- const findSearchBox = () => wrapper.findComponent(GlSearchBoxByType);
- const findInfiniteScroll = () => wrapper.findComponent(GlInfiniteScroll);
- const defaultBranchInDropdown = () => findDropdownItems().at(0);
+ const findGlCollapsibleListbox = () => wrapper.findComponent(GlCollapsibleListbox);
+ const findGlListboxItems = () => wrapper.findAllComponents(GlListboxItem);
+ const defaultBranchInDropdown = () => findGlListboxItems().at(0);
const setAvailableBranchesMock = (availableBranches) => {
mockAvailableBranchQuery.mockResolvedValue(availableBranches);
@@ -112,11 +104,7 @@ describe('Pipeline editor branch switcher', () => {
});
it('disables the dropdown', () => {
- expect(findDropdown().props('disabled')).toBe(true);
- });
-
- it('shows loading icon', () => {
- expect(findLoadingIcon().exists()).toBe(true);
+ expect(findGlCollapsibleListbox().props('disabled')).toBe(true);
});
});
@@ -126,29 +114,25 @@ describe('Pipeline editor branch switcher', () => {
await createComponent();
});
- it('does not render the loading icon', () => {
- expect(findLoadingIcon().exists()).toBe(false);
- });
-
it('renders search box', () => {
- expect(findSearchBox().exists()).toBe(true);
+ expect(findGlCollapsibleListbox().props().searchable).toBe(true);
});
it('renders list of branches', () => {
- expect(findDropdown().exists()).toBe(true);
- expect(findDropdownItems()).toHaveLength(mockTotalBranchResults);
+ expect(findGlCollapsibleListbox().exists()).toBe(true);
+ expect(findGlListboxItems()).toHaveLength(mockTotalBranchResults);
});
it('renders current branch with a check mark', () => {
expect(defaultBranchInDropdown().text()).toBe(mockDefaultBranch);
- expect(defaultBranchInDropdown().props('isChecked')).toBe(true);
+ expect(defaultBranchInDropdown().props('isSelected')).toBe(true);
});
it('does not render check mark for other branches', () => {
- const nonDefaultBranch = findDropdownItems().at(1);
+ const nonDefaultBranch = findGlListboxItems().at(1);
expect(nonDefaultBranch.text()).not.toBe(mockDefaultBranch);
- expect(nonDefaultBranch.props('isChecked')).toBe(false);
+ expect(nonDefaultBranch.props('isSelected')).toBe(false);
});
});
@@ -159,7 +143,7 @@ describe('Pipeline editor branch switcher', () => {
});
it('does not render dropdown', () => {
- expect(findDropdown().props('disabled')).toBe(true);
+ expect(findGlCollapsibleListbox().props('disabled')).toBe(true);
});
it('shows an error message', () => {
@@ -175,8 +159,8 @@ describe('Pipeline editor branch switcher', () => {
});
it('updates session history when selecting a different branch', async () => {
- const branch = findDropdownItems().at(1);
- branch.vm.$emit('click');
+ const branch = findGlListboxItems().at(1);
+ findGlCollapsibleListbox().vm.$emit('select', branch.text());
await waitForPromises();
expect(window.history.pushState).toHaveBeenCalled();
@@ -184,7 +168,7 @@ describe('Pipeline editor branch switcher', () => {
});
it('does not update session history when selecting current branch', async () => {
- const branch = findDropdownItems().at(0);
+ const branch = findGlListboxItems().at(0);
branch.vm.$emit('click');
await waitForPromises();
@@ -192,21 +176,21 @@ describe('Pipeline editor branch switcher', () => {
expect(window.history.pushState).not.toHaveBeenCalled();
});
- it('emits the refetchContent event when selecting a different branch', async () => {
- const branch = findDropdownItems().at(1);
+ it('emits the `refetchContent` event when selecting a different branch', async () => {
+ const branch = findGlListboxItems().at(1);
expect(branch.text()).not.toBe(mockDefaultBranch);
expect(wrapper.emitted('refetchContent')).toBeUndefined();
- branch.vm.$emit('click');
+ findGlCollapsibleListbox().vm.$emit('select', branch.text());
await waitForPromises();
expect(wrapper.emitted('refetchContent')).toBeDefined();
expect(wrapper.emitted('refetchContent')).toHaveLength(1);
});
- it('does not emit the refetchContent event when selecting the current branch', async () => {
- const branch = findDropdownItems().at(0);
+ it('does not emit the `refetchContent` event when selecting the current branch', async () => {
+ const branch = findGlListboxItems().at(0);
expect(branch.text()).toBe(mockDefaultBranch);
expect(wrapper.emitted('refetchContent')).toBeUndefined();
@@ -223,11 +207,11 @@ describe('Pipeline editor branch switcher', () => {
await waitForPromises();
});
- it('emits `select-branch` event and does not switch branch', async () => {
+ it('emits `select-branch` event and does not switch branch', () => {
expect(wrapper.emitted('select-branch')).toBeUndefined();
- const branch = findDropdownItems().at(1);
- await branch.vm.$emit('click');
+ const branch = findGlListboxItems().at(1);
+ findGlCollapsibleListbox().vm.$emit('select', branch.text());
expect(wrapper.emitted('select-branch')).toEqual([[branch.text()]]);
expect(wrapper.emitted('refetchContent')).toBeUndefined();
@@ -248,7 +232,7 @@ describe('Pipeline editor branch switcher', () => {
it('shows error message on fetch error', async () => {
mockAvailableBranchQuery.mockResolvedValue(new Error());
- findSearchBox().vm.$emit('input', 'te');
+ findGlCollapsibleListbox().vm.$emit('search', 'te');
await waitForPromises();
testErrorHandling();
@@ -260,7 +244,8 @@ describe('Pipeline editor branch switcher', () => {
});
it('calls query with correct variables', async () => {
- findSearchBox().vm.$emit('input', 'te');
+ findGlCollapsibleListbox().vm.$emit('search', 'te');
+
await waitForPromises();
expect(mockAvailableBranchQuery).toHaveBeenCalledWith({
@@ -272,35 +257,35 @@ describe('Pipeline editor branch switcher', () => {
});
it('fetches new list of branches', async () => {
- expect(findDropdownItems()).toHaveLength(mockTotalBranchResults);
+ expect(findGlListboxItems()).toHaveLength(mockTotalBranchResults);
- findSearchBox().vm.$emit('input', 'te');
+ findGlCollapsibleListbox().vm.$emit('search', 'te');
await waitForPromises();
- expect(findDropdownItems()).toHaveLength(mockTotalSearchResults);
+ expect(findGlListboxItems()).toHaveLength(mockTotalSearchResults);
});
it('does not hide dropdown when search result is empty', async () => {
mockAvailableBranchQuery.mockResolvedValue(mockEmptySearchBranches);
- findSearchBox().vm.$emit('input', 'aaaaa');
+ findGlCollapsibleListbox().vm.$emit('search', 'aaaa');
await waitForPromises();
- expect(findDropdown().exists()).toBe(true);
- expect(findDropdownItems()).toHaveLength(0);
+ expect(findGlCollapsibleListbox().exists()).toBe(true);
+ expect(findGlListboxItems()).toHaveLength(0);
});
});
describe('without a search term', () => {
beforeEach(async () => {
mockAvailableBranchQuery.mockResolvedValue(mockSearchBranches);
- findSearchBox().vm.$emit('input', 'te');
+ findGlCollapsibleListbox().vm.$emit('search', 'te');
await waitForPromises();
mockAvailableBranchQuery.mockResolvedValue(generateMockProjectBranches());
});
it('calls query with correct variables', async () => {
- findSearchBox().vm.$emit('input', '');
+ findGlCollapsibleListbox().vm.$emit('search', '');
await waitForPromises();
expect(mockAvailableBranchQuery).toHaveBeenCalledWith({
@@ -312,71 +297,34 @@ describe('Pipeline editor branch switcher', () => {
});
it('fetches new list of branches', async () => {
- expect(findDropdownItems()).toHaveLength(mockTotalSearchResults);
+ expect(findGlListboxItems()).toHaveLength(mockTotalSearchResults);
- findSearchBox().vm.$emit('input', '');
+ findGlCollapsibleListbox().vm.$emit('search', '');
await waitForPromises();
- expect(findDropdownItems()).toHaveLength(mockTotalBranchResults);
+ expect(findGlListboxItems()).toHaveLength(mockTotalBranchResults);
});
});
});
describe('when scrolling to the bottom of the list', () => {
beforeEach(async () => {
- setAvailableBranchesMock(generateMockProjectBranches());
- await createComponent();
+ createComponent();
+ await waitForPromises();
});
afterEach(() => {
mockAvailableBranchQuery.mockClear();
});
- describe('when search term is empty', () => {
- it('fetches more branches', async () => {
- expect(mockAvailableBranchQuery).toHaveBeenCalledTimes(1);
-
- setAvailableBranchesMock(generateMockProjectBranches('new-'));
- findInfiniteScroll().vm.$emit('bottomReached');
- await waitForPromises();
-
- expect(mockAvailableBranchQuery).toHaveBeenCalledTimes(2);
- });
-
- it('calls the query with the correct variables', async () => {
- setAvailableBranchesMock(generateMockProjectBranches('new-'));
- findInfiniteScroll().vm.$emit('bottomReached');
- await waitForPromises();
-
- expect(mockAvailableBranchQuery).toHaveBeenCalledWith({
- limit: mockBranchPaginationLimit,
- offset: mockBranchPaginationLimit, // offset changed
- projectFullPath: mockProjectFullPath,
- searchPattern: '*',
- });
- });
-
- it('shows error message on fetch error', async () => {
- mockAvailableBranchQuery.mockResolvedValue(new Error());
-
- findInfiniteScroll().vm.$emit('bottomReached');
- await waitForPromises();
-
- testErrorHandling();
- });
- });
-
describe('when search term exists', () => {
it('does not fetch more branches', async () => {
- findSearchBox().vm.$emit('input', 'te');
+ findGlCollapsibleListbox().vm.$emit('search', 'new');
await waitForPromises();
expect(mockAvailableBranchQuery).toHaveBeenCalledTimes(2);
mockAvailableBranchQuery.mockClear();
- findInfiniteScroll().vm.$emit('bottomReached');
- await waitForPromises();
-
expect(mockAvailableBranchQuery).not.toHaveBeenCalled();
});
});
diff --git a/spec/frontend/ci/pipeline_mini_graph/legacy_pipeline_stage_spec.js b/spec/frontend/ci/pipeline_mini_graph/legacy_pipeline_stage_spec.js
index 4b357a9fc7c..87df7676bf1 100644
--- a/spec/frontend/ci/pipeline_mini_graph/legacy_pipeline_stage_spec.js
+++ b/spec/frontend/ci/pipeline_mini_graph/legacy_pipeline_stage_spec.js
@@ -2,7 +2,7 @@ import { GlDropdown } from '@gitlab/ui';
import { nextTick } from 'vue';
import { mount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
-import CiBadgeLink from '~/vue_shared/components/ci_badge_link.vue';
+import CiIcon from '~/vue_shared/components/ci_icon.vue';
import axios from '~/lib/utils/axios_utils';
import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import LegacyPipelineStage from '~/ci/pipeline_mini_graph/legacy_pipeline_stage.vue';
@@ -52,7 +52,7 @@ describe('Pipelines stage component', () => {
});
const findCiActionBtn = () => wrapper.find('.js-ci-action');
- const findCiIcon = () => wrapper.findComponent(CiBadgeLink);
+ const findCiIcon = () => wrapper.findComponent(CiIcon);
const findDropdown = () => wrapper.findComponent(GlDropdown);
const findDropdownToggle = () => wrapper.find('button.dropdown-toggle');
const findDropdownMenu = () =>
diff --git a/spec/frontend/ci/pipeline_mini_graph/linked_pipelines_mini_list_spec.js b/spec/frontend/ci/pipeline_mini_graph/linked_pipelines_mini_list_spec.js
index 3c9d235bfcc..55ce3c79039 100644
--- a/spec/frontend/ci/pipeline_mini_graph/linked_pipelines_mini_list_spec.js
+++ b/spec/frontend/ci/pipeline_mini_graph/linked_pipelines_mini_list_spec.js
@@ -51,7 +51,7 @@ describe('Linked pipeline mini list', () => {
});
it('should render the correct ci status icon', () => {
- expect(findCiIcon().classes('ci-status-icon-running')).toBe(true);
+ expect(wrapper.find('[data-testid="status_running_borderless-icon"]').exists()).toBe(true);
});
it('should have an activated tooltip', () => {
@@ -95,7 +95,7 @@ describe('Linked pipeline mini list', () => {
});
it('should render the correct ci status icon', () => {
- expect(findCiIcon().classes('ci-status-icon-running')).toBe(true);
+ expect(wrapper.find('[data-testid="status_running_borderless-icon"]').exists()).toBe(true);
});
it('should have an activated tooltip', () => {
diff --git a/spec/frontend/ci/pipeline_schedules/components/table/cells/pipeline_schedule_last_pipeline_spec.js b/spec/frontend/ci/pipeline_schedules/components/table/cells/pipeline_schedule_last_pipeline_spec.js
index ae069145292..b79e7c6e251 100644
--- a/spec/frontend/ci/pipeline_schedules/components/table/cells/pipeline_schedule_last_pipeline_spec.js
+++ b/spec/frontend/ci/pipeline_schedules/components/table/cells/pipeline_schedule_last_pipeline_spec.js
@@ -1,5 +1,5 @@
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import CiBadgeLink from '~/vue_shared/components/ci_badge_link.vue';
+import CiIcon from '~/vue_shared/components/ci_icon.vue';
import PipelineScheduleLastPipeline from '~/ci/pipeline_schedules/components/table/cells/pipeline_schedule_last_pipeline.vue';
import { mockPipelineScheduleNodes } from '../../../mock_data';
@@ -18,16 +18,14 @@ describe('Pipeline schedule last pipeline', () => {
});
};
- const findCIBadgeLink = () => wrapper.findComponent(CiBadgeLink);
+ const findCiIcon = () => wrapper.findComponent(CiIcon);
const findStatusText = () => wrapper.findByTestId('pipeline-schedule-status-text');
it('displays pipeline status', () => {
createComponent();
- expect(findCIBadgeLink().exists()).toBe(true);
- expect(findCIBadgeLink().props('status')).toBe(
- defaultProps.schedule.lastPipeline.detailedStatus,
- );
+ expect(findCiIcon().exists()).toBe(true);
+ expect(findCiIcon().props('status')).toBe(defaultProps.schedule.lastPipeline.detailedStatus);
expect(findStatusText().exists()).toBe(false);
});
@@ -35,6 +33,6 @@ describe('Pipeline schedule last pipeline', () => {
createComponent({ schedule: mockPipelineScheduleNodes[0] });
expect(findStatusText().text()).toBe('None');
- expect(findCIBadgeLink().exists()).toBe(false);
+ expect(findCiIcon().exists()).toBe(false);
});
});
diff --git a/spec/frontend/ci/pipelines_page/components/empty_state/ios_templates_spec.js b/spec/frontend/ci/pipelines_page/components/empty_state/ios_templates_spec.js
deleted file mode 100644
index 8620d41886e..00000000000
--- a/spec/frontend/ci/pipelines_page/components/empty_state/ios_templates_spec.js
+++ /dev/null
@@ -1,133 +0,0 @@
-import '~/commons';
-import { nextTick } from 'vue';
-import { GlPopover, GlButton } from '@gitlab/ui';
-import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import RunnerInstructionsModal from '~/vue_shared/components/runner_instructions/runner_instructions_modal.vue';
-import IosTemplates from '~/ci/pipelines_page/components/empty_state/ios_templates.vue';
-import CiTemplates from '~/ci/pipelines_page/components/empty_state/ci_templates.vue';
-
-const pipelineEditorPath = '/-/ci/editor';
-const registrationToken = 'SECRET_TOKEN';
-const iOSTemplateName = 'iOS-Fastlane';
-
-describe('iOS Templates', () => {
- let wrapper;
-
- const createWrapper = (providedPropsData = {}) => {
- return shallowMountExtended(IosTemplates, {
- provide: {
- pipelineEditorPath,
- iosRunnersAvailable: true,
- ...providedPropsData,
- },
- propsData: {
- registrationToken,
- },
- stubs: {
- GlButton,
- },
- });
- };
-
- const findIosTemplate = () => wrapper.findComponent(CiTemplates);
- const findRunnerInstructionsModal = () => wrapper.findComponent(RunnerInstructionsModal);
- const findRunnerInstructionsPopover = () => wrapper.findComponent(GlPopover);
- const findRunnerSetupTodoEmoji = () => wrapper.findByTestId('runner-setup-marked-todo');
- const findRunnerSetupCompletedEmoji = () => wrapper.findByTestId('runner-setup-marked-completed');
- const findSetupRunnerLink = () => wrapper.findByText('Set up a runner');
- const configurePipelineLink = () => wrapper.findByTestId('configure-pipeline-link');
-
- describe('when ios runners are not available', () => {
- beforeEach(() => {
- wrapper = createWrapper({ iosRunnersAvailable: false });
- });
-
- describe('the runner setup section', () => {
- it('marks the section as todo', () => {
- expect(findRunnerSetupTodoEmoji().isVisible()).toBe(true);
- expect(findRunnerSetupCompletedEmoji().isVisible()).toBe(false);
- });
-
- it('renders the setup runner link', () => {
- expect(findSetupRunnerLink().exists()).toBe(true);
- });
-
- it('renders the runner instructions modal with a popover once clicked', async () => {
- findSetupRunnerLink().element.parentElement.click();
-
- await nextTick();
-
- expect(findRunnerInstructionsModal().exists()).toBe(true);
- expect(findRunnerInstructionsModal().props('registrationToken')).toBe(registrationToken);
- expect(findRunnerInstructionsModal().props('defaultPlatformName')).toBe('osx');
-
- findRunnerInstructionsModal().vm.$emit('shown');
-
- await nextTick();
-
- expect(findRunnerInstructionsPopover().exists()).toBe(true);
- });
- });
-
- describe('the configure pipeline section', () => {
- it('has a disabled link button', () => {
- expect(configurePipelineLink().props('disabled')).toBe(true);
- });
- });
-
- describe('the ios-Fastlane template', () => {
- it('renders the template', () => {
- expect(findIosTemplate().props('filterTemplates')).toStrictEqual([iOSTemplateName]);
- });
-
- it('has a disabled link button', () => {
- expect(findIosTemplate().props('disabled')).toBe(true);
- });
- });
- });
-
- describe('when ios runners are available', () => {
- beforeEach(() => {
- wrapper = createWrapper();
- });
-
- describe('the runner setup section', () => {
- it('marks the section as completed', () => {
- expect(findRunnerSetupTodoEmoji().isVisible()).toBe(false);
- expect(findRunnerSetupCompletedEmoji().isVisible()).toBe(true);
- });
-
- it('does not render the setup runner link', () => {
- expect(findSetupRunnerLink().exists()).toBe(false);
- });
- });
-
- describe('the configure pipeline section', () => {
- it('has an enabled link button', () => {
- expect(configurePipelineLink().props('disabled')).toBe(false);
- });
-
- it('links to the pipeline editor with the right template', () => {
- expect(configurePipelineLink().attributes('href')).toBe(
- `${pipelineEditorPath}?template=${iOSTemplateName}`,
- );
- });
- });
-
- describe('the ios-Fastlane template', () => {
- it('renders the template', () => {
- expect(findIosTemplate().props('filterTemplates')).toStrictEqual([iOSTemplateName]);
- });
-
- it('has an enabled link button', () => {
- expect(findIosTemplate().props('disabled')).toBe(false);
- });
-
- it('links to the pipeline editor with the right template', () => {
- expect(configurePipelineLink().attributes('href')).toBe(
- `${pipelineEditorPath}?template=${iOSTemplateName}`,
- );
- });
- });
- });
-});
diff --git a/spec/frontend/ci/pipelines_page/components/empty_state/no_ci_empty_state_spec.js b/spec/frontend/ci/pipelines_page/components/empty_state/no_ci_empty_state_spec.js
index 0c42723f753..ea47edb6842 100644
--- a/spec/frontend/ci/pipelines_page/components/empty_state/no_ci_empty_state_spec.js
+++ b/spec/frontend/ci/pipelines_page/components/empty_state/no_ci_empty_state_spec.js
@@ -1,11 +1,8 @@
import '~/commons';
import { shallowMount } from '@vue/test-utils';
import { GlEmptyState } from '@gitlab/ui';
-import { stubExperiments } from 'helpers/experimentation_helper';
import EmptyState from '~/ci/pipelines_page/components/empty_state/no_ci_empty_state.vue';
-import GitlabExperiment from '~/experimentation/components/gitlab_experiment.vue';
import PipelinesCiTemplates from '~/ci/pipelines_page/components/empty_state/pipelines_ci_templates.vue';
-import IosTemplates from '~/ci/pipelines_page/components/empty_state/ios_templates.vue';
describe('Pipelines Empty State', () => {
let wrapper;
@@ -13,7 +10,6 @@ describe('Pipelines Empty State', () => {
const findIllustration = () => wrapper.find('img');
const findButton = () => wrapper.find('a');
const pipelinesCiTemplates = () => wrapper.findComponent(PipelinesCiTemplates);
- const iosTemplates = () => wrapper.findComponent(IosTemplates);
const createWrapper = (props = {}) => {
wrapper = shallowMount(EmptyState, {
@@ -30,40 +26,17 @@ describe('Pipelines Empty State', () => {
},
stubs: {
GlEmptyState,
- GitlabExperiment,
},
});
};
describe('when user can configure CI', () => {
- describe('when the ios_specific_templates experiment is active', () => {
- beforeEach(() => {
- stubExperiments({ ios_specific_templates: 'candidate' });
- createWrapper();
- });
-
- it('should render the iOS templates', () => {
- expect(iosTemplates().exists()).toBe(true);
- });
-
- it('should not render the CI/CD templates', () => {
- expect(pipelinesCiTemplates().exists()).toBe(false);
- });
+ beforeEach(() => {
+ createWrapper();
});
- describe('when the ios_specific_templates experiment is inactive', () => {
- beforeEach(() => {
- stubExperiments({ ios_specific_templates: 'control' });
- createWrapper();
- });
-
- it('should render the CI/CD templates', () => {
- expect(pipelinesCiTemplates().exists()).toBe(true);
- });
-
- it('should not render the iOS templates', () => {
- expect(iosTemplates().exists()).toBe(false);
- });
+ it('should render the CI/CD templates', () => {
+ expect(pipelinesCiTemplates().exists()).toBe(true);
});
});
diff --git a/spec/frontend/ci/pipelines_page/components/pipeline_labels_spec.js b/spec/frontend/ci/pipelines_page/components/pipeline_labels_spec.js
index 6b0d5b18f7d..a660994ac8b 100644
--- a/spec/frontend/ci/pipelines_page/components/pipeline_labels_spec.js
+++ b/spec/frontend/ci/pipelines_page/components/pipeline_labels_spec.js
@@ -2,6 +2,7 @@ import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { trimText } from 'helpers/text_helper';
import PipelineLabelsComponent from '~/ci/pipelines_page/components/pipeline_labels.vue';
import { mockPipeline } from 'jest/ci/pipeline_details/mock_data';
+import { SCHEDULE_ORIGIN, API_ORIGIN } from '~/ci/pipelines_page/constants';
const projectPath = 'test/test';
@@ -9,6 +10,7 @@ describe('Pipeline label component', () => {
let wrapper;
const findScheduledTag = () => wrapper.findByTestId('pipeline-url-scheduled');
+ const findTriggeredTag = () => wrapper.findByTestId('pipeline-url-triggered');
const findLatestTag = () => wrapper.findByTestId('pipeline-url-latest');
const findYamlTag = () => wrapper.findByTestId('pipeline-url-yaml');
const findStuckTag = () => wrapper.findByTestId('pipeline-url-stuck');
@@ -19,6 +21,7 @@ describe('Pipeline label component', () => {
const findFailureTag = () => wrapper.findByTestId('pipeline-url-failure');
const findForkTag = () => wrapper.findByTestId('pipeline-url-fork');
const findTrainTag = () => wrapper.findByTestId('pipeline-url-train');
+ const findApiTag = () => wrapper.findByTestId('pipeline-api-badge');
const defaultProps = mockPipeline(projectPath);
@@ -41,6 +44,7 @@ describe('Pipeline label component', () => {
expect(findAutoDevopsTag().exists()).toBe(false);
expect(findFailureTag().exists()).toBe(false);
expect(findScheduledTag().exists()).toBe(false);
+ expect(findTriggeredTag().exists()).toBe(false);
expect(findForkTag().exists()).toBe(false);
expect(findTrainTag().exists()).toBe(false);
expect(findMergedResultsTag().exists()).toBe(false);
@@ -121,14 +125,28 @@ describe('Pipeline label component', () => {
it('should render scheduled badge when pipeline was triggered by a schedule', () => {
const scheduledPipeline = defaultProps.pipeline;
- scheduledPipeline.source = 'schedule';
+ scheduledPipeline.source = SCHEDULE_ORIGIN;
createComponent({
...scheduledPipeline,
});
expect(findScheduledTag().exists()).toBe(true);
- expect(findScheduledTag().text()).toContain('Scheduled');
+ expect(findScheduledTag().text()).toContain('scheduled');
+ });
+
+ it('should render triggered badge when pipeline was triggered by a trigger', () => {
+ const triggeredPipeline = {
+ ...defaultProps.pipeline,
+ source: 'trigger',
+ };
+
+ createComponent({
+ pipeline: triggeredPipeline,
+ });
+
+ expect(findTriggeredTag().exists()).toBe(true);
+ expect(findTriggeredTag().text()).toBe('trigger token');
});
it('should render the fork badge when the pipeline was run in a fork', () => {
@@ -201,4 +219,26 @@ describe('Pipeline label component', () => {
expect(findMergedResultsTag().exists()).toBe(false);
});
+
+ it.each`
+ display | source
+ ${true} | ${API_ORIGIN}
+ ${false} | ${SCHEDULE_ORIGIN}
+ `(
+ 'should display the api badge: $display, when the pipeline has a source of $source',
+ ({ display, source }) => {
+ const apiPipeline = defaultProps.pipeline;
+ apiPipeline.source = source;
+
+ createComponent({
+ ...apiPipeline,
+ });
+
+ if (display) {
+ expect(findApiTag().text()).toBe(API_ORIGIN);
+ } else {
+ expect(findApiTag().exists()).toBe(false);
+ }
+ },
+ );
});
diff --git a/spec/frontend/ci/pipelines_page/components/pipeline_triggerer_spec.js b/spec/frontend/ci/pipelines_page/components/pipeline_triggerer_spec.js
index cb04171f031..a4780cddc3c 100644
--- a/spec/frontend/ci/pipelines_page/components/pipeline_triggerer_spec.js
+++ b/spec/frontend/ci/pipelines_page/components/pipeline_triggerer_spec.js
@@ -29,7 +29,6 @@ describe('Pipelines Triggerer', () => {
const findAvatarLink = () => wrapper.findComponent(GlAvatarLink);
const findAvatar = () => wrapper.findComponent(GlAvatar);
- const findTriggerer = () => wrapper.findByText('API');
describe('when user was a triggerer', () => {
beforeEach(() => {
@@ -42,7 +41,6 @@ describe('Pipelines Triggerer', () => {
it('should render only user avatar', () => {
expect(findAvatarLink().exists()).toBe(true);
- expect(findTriggerer().exists()).toBe(false);
});
it('should set correct props on avatar link component', () => {
@@ -62,15 +60,4 @@ describe('Pipelines Triggerer', () => {
expect(findAvatar().attributes().src).toBe(mockData.pipeline.user.avatar_url);
});
});
-
- describe('when API was a triggerer', () => {
- beforeEach(() => {
- createComponent({ pipeline: {} });
- });
-
- it('should render label only', () => {
- expect(findAvatarLink().exists()).toBe(false);
- expect(findTriggerer().exists()).toBe(true);
- });
- });
});
diff --git a/spec/frontend/ci/pipelines_page/pipelines_spec.js b/spec/frontend/ci/pipelines_page/pipelines_spec.js
index fd95f98e7f8..97192058ff6 100644
--- a/spec/frontend/ci/pipelines_page/pipelines_spec.js
+++ b/spec/frontend/ci/pipelines_page/pipelines_spec.js
@@ -7,11 +7,11 @@ import {
GlPagination,
GlCollapsibleListbox,
} from '@gitlab/ui';
-import * as Sentry from '@sentry/browser';
import { mount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import { chunk } from 'lodash';
-import { nextTick } from 'vue';
+import Vue, { nextTick } from 'vue';
+import VueApollo from 'vue-apollo';
import mockPipelinesResponse from 'test_fixtures/pipelines/pipelines.json';
import setWindowLocation from 'helpers/set_window_location_helper';
import { TEST_HOST } from 'helpers/test_constants';
@@ -19,6 +19,7 @@ import { mockTracking } from 'helpers/tracking_helper';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import createMockApollo from 'helpers/mock_apollo_helper';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import Api from '~/api';
import { createAlert, VARIANT_WARNING } from '~/alert';
import setSortPreferenceMutation from '~/issues/list/queries/set_sort_preference.mutation.graphql';
@@ -28,7 +29,12 @@ import NavigationControls from '~/ci/pipelines_page/components/nav_controls.vue'
import PipelinesComponent from '~/ci/pipelines_page/pipelines.vue';
import PipelinesCiTemplates from '~/ci/pipelines_page/components/empty_state/pipelines_ci_templates.vue';
import PipelinesTableComponent from '~/ci/common/pipelines_table.vue';
-import { PIPELINE_IID_KEY, RAW_TEXT_WARNING, TRACKING_CATEGORIES } from '~/ci/constants';
+import {
+ PIPELINE_ID_KEY,
+ PIPELINE_IID_KEY,
+ RAW_TEXT_WARNING,
+ TRACKING_CATEGORIES,
+} from '~/ci/constants';
import Store from '~/ci/pipeline_details/stores/pipelines_store';
import NavigationTabs from '~/vue_shared/components/navigation_tabs.vue';
import TablePagination from '~/vue_shared/components/pagination/table_pagination.vue';
@@ -36,11 +42,12 @@ import {
setIdTypePreferenceMutationResponse,
setIdTypePreferenceMutationResponseWithErrors,
} from 'jest/issues/list/mock_data';
-
import { stageReply } from 'jest/ci/pipeline_mini_graph/mock_data';
import { users, mockSearch, branches } from '../pipeline_details/mock_data';
-jest.mock('@sentry/browser');
+Vue.use(VueApollo);
+
+jest.mock('~/sentry/sentry_browser_wrapper');
jest.mock('~/alert');
const mockProjectPath = 'twitter/flight';
@@ -372,6 +379,8 @@ describe('Pipelines', () => {
beforeEach(() => {
gon.current_user_id = 1;
+
+ trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
});
it('should change the text to Show Pipeline IID', async () => {
@@ -384,6 +393,25 @@ describe('Pipelines', () => {
expect(findPipelineUrlLinks().at(0).text()).toBe(`#${mockFilteredPipeline.iid}`);
});
+ it('tracks the iid usage of the ID/IID dropdown', async () => {
+ findPipelineKeyCollapsibleBox().vm.$emit('select', PIPELINE_IID_KEY);
+
+ await waitForPromises();
+
+ expect(trackingSpy).toHaveBeenCalledWith(undefined, 'pipelines_display_options', {
+ label: TRACKING_CATEGORIES.listbox,
+ property: 'iid',
+ });
+ });
+
+ it('does not track the id usage of the ID/IID dropdown', async () => {
+ findPipelineKeyCollapsibleBox().vm.$emit('select', PIPELINE_ID_KEY);
+
+ await waitForPromises();
+
+ expect(trackingSpy).not.toHaveBeenCalled();
+ });
+
it('calls mutation to save idType preference', () => {
mutationMock = jest.fn().mockResolvedValue(setIdTypePreferenceMutationResponse);
createComponent();
diff --git a/spec/frontend/ci/runner/admin_new_runner_app/admin_new_runner_app_spec.js b/spec/frontend/ci/runner/admin_new_runner_app/admin_new_runner_app_spec.js
index 75bca68b888..03958381d75 100644
--- a/spec/frontend/ci/runner/admin_new_runner_app/admin_new_runner_app_spec.js
+++ b/spec/frontend/ci/runner/admin_new_runner_app/admin_new_runner_app_spec.js
@@ -6,7 +6,6 @@ import { createAlert, VARIANT_SUCCESS } from '~/alert';
import AdminNewRunnerApp from '~/ci/runner/admin_new_runner/admin_new_runner_app.vue';
import RegistrationCompatibilityAlert from '~/ci/runner/components/registration/registration_compatibility_alert.vue';
-import RegistrationFeedbackBanner from '~/ci/runner/components/registration/registration_feedback_banner.vue';
import { saveAlertToLocalStorage } from '~/ci/runner/local_storage_alert/save_alert_to_local_storage';
import RunnerPlatformsRadioGroup from '~/ci/runner/components/runner_platforms_radio_group.vue';
import {
@@ -32,7 +31,6 @@ describe('AdminNewRunnerApp', () => {
let wrapper;
const findRunnerPlatformsRadioGroup = () => wrapper.findComponent(RunnerPlatformsRadioGroup);
- const findRegistrationFeedbackBanner = () => wrapper.findComponent(RegistrationFeedbackBanner);
const findRegistrationCompatibilityAlert = () =>
wrapper.findComponent(RegistrationCompatibilityAlert);
const findRunnerCreateForm = () => wrapper.findComponent(RunnerCreateForm);
@@ -49,10 +47,6 @@ describe('AdminNewRunnerApp', () => {
createComponent();
});
- it('shows a registration feedback banner', () => {
- expect(findRegistrationFeedbackBanner().exists()).toBe(true);
- });
-
it('shows a registration compatibility alert', () => {
expect(findRegistrationCompatibilityAlert().props('alertKey')).toBe(INSTANCE_TYPE);
});
diff --git a/spec/frontend/ci/runner/admin_runners/admin_runners_app_spec.js b/spec/frontend/ci/runner/admin_runners/admin_runners_app_spec.js
index bc28147db27..4f5f9c43cb4 100644
--- a/spec/frontend/ci/runner/admin_runners/admin_runners_app_spec.js
+++ b/spec/frontend/ci/runner/admin_runners/admin_runners_app_spec.js
@@ -43,6 +43,7 @@ import {
PARAM_KEY_PAUSED,
PARAM_KEY_STATUS,
PARAM_KEY_TAG,
+ PARAM_KEY_VERSION,
STATUS_ONLINE,
DEFAULT_MEMBERSHIP,
RUNNER_PAGE_SIZE,
@@ -255,6 +256,10 @@ describe('AdminRunnersApp', () => {
options: expect.any(Array),
}),
expect.objectContaining({
+ type: PARAM_KEY_VERSION,
+ title: 'Version starts with',
+ }),
+ expect.objectContaining({
type: PARAM_KEY_TAG,
recentSuggestionsStorageKey: `${ADMIN_FILTERED_SEARCH_NAMESPACE}-recent-tags`,
}),
diff --git a/spec/frontend/ci/runner/components/cells/runner_summary_cell_spec.js b/spec/frontend/ci/runner/components/cells/runner_summary_cell_spec.js
index bc77b7b89dd..27fb288c462 100644
--- a/spec/frontend/ci/runner/components/cells/runner_summary_cell_spec.js
+++ b/spec/frontend/ci/runner/components/cells/runner_summary_cell_spec.js
@@ -1,21 +1,15 @@
import { GlSprintf } from '@gitlab/ui';
-import { __, sprintf } from '~/locale';
+import { __ } from '~/locale';
import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
import RunnerSummaryCell from '~/ci/runner/components/cells/runner_summary_cell.vue';
import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
+import RunnerCreatedAt from '~/ci/runner/components/runner_created_at.vue';
import RunnerManagersBadge from '~/ci/runner/components/runner_managers_badge.vue';
import RunnerTags from '~/ci/runner/components/runner_tags.vue';
import RunnerSummaryField from '~/ci/runner/components/cells/runner_summary_field.vue';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
-import {
- INSTANCE_TYPE,
- I18N_INSTANCE_TYPE,
- PROJECT_TYPE,
- I18N_CREATED_AT_LABEL,
- I18N_CREATED_AT_BY_LABEL,
-} from '~/ci/runner/constants';
+import { INSTANCE_TYPE, I18N_INSTANCE_TYPE, PROJECT_TYPE } from '~/ci/runner/constants';
import { allRunnersWithCreatorData } from '../../mock_data';
@@ -182,45 +176,11 @@ describe('RunnerTypeCell', () => {
expect(findRunnerSummaryField('pipeline').text()).toContain('1,000+');
});
- describe('Displays creation info', () => {
- const findCreatedTime = () => findRunnerSummaryField('calendar').findComponent(TimeAgo);
-
- it('Displays created at ...', () => {
- createComponent({
- runner: { createdBy: null },
- });
-
- expect(findRunnerSummaryField('calendar').text()).toMatchInterpolatedText(
- sprintf(I18N_CREATED_AT_LABEL, {
- timeAgo: findCreatedTime().text(),
- }),
- );
- expect(findCreatedTime().props('time')).toBe(mockRunner.createdAt);
- });
-
- it('Displays created at ... by ...', () => {
- createComponent({ mountFn: mountExtended });
-
- expect(findRunnerSummaryField('calendar').text()).toMatchInterpolatedText(
- sprintf(I18N_CREATED_AT_BY_LABEL, {
- timeAgo: findCreatedTime().text(),
- avatar: mockRunner.createdBy.username,
- }),
- );
-
- expect(findCreatedTime().props('time')).toBe(mockRunner.createdAt);
- });
-
- it('Displays creator avatar', () => {
- const { name, avatarUrl, webUrl, username } = mockRunner.createdBy;
+ it('Displays creation info', () => {
+ createComponent();
- expect(wrapper.findComponent(UserAvatarLink).props()).toMatchObject({
- imgAlt: expect.stringContaining(name),
- imgSrc: avatarUrl,
- linkHref: webUrl,
- tooltipText: username,
- });
- });
+ const createdAt = findRunnerSummaryField('calendar').findComponent(RunnerCreatedAt);
+ expect(createdAt.props('runner')).toEqual(mockRunner);
});
it('Displays tag list', () => {
diff --git a/spec/frontend/ci/runner/components/registration/registration_dropdown_spec.js b/spec/frontend/ci/runner/components/registration/registration_dropdown_spec.js
index 3fb845b186a..f8ef0bdad51 100644
--- a/spec/frontend/ci/runner/components/registration/registration_dropdown_spec.js
+++ b/spec/frontend/ci/runner/components/registration/registration_dropdown_spec.js
@@ -178,7 +178,7 @@ describe('RegistrationDropdown', () => {
mountExtended,
);
- expect(findRegistrationTokenInput().element.type).toBe('password');
+ expect(findRegistrationTokenInput().classes()).toContain('input-copy-show-disc');
});
});
diff --git a/spec/frontend/ci/runner/components/registration/registration_feedback_banner_spec.js b/spec/frontend/ci/runner/components/registration/registration_feedback_banner_spec.js
deleted file mode 100644
index fa6b7ad7c63..00000000000
--- a/spec/frontend/ci/runner/components/registration/registration_feedback_banner_spec.js
+++ /dev/null
@@ -1,52 +0,0 @@
-import { GlBanner } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
-import RegistrationFeedbackBanner from '~/ci/runner/components/registration/registration_feedback_banner.vue';
-import UserCalloutDismisser from '~/vue_shared/components/user_callout_dismisser.vue';
-import { makeMockUserCalloutDismisser } from 'helpers/mock_user_callout_dismisser';
-
-describe('Runner registration feeback banner', () => {
- let wrapper;
- let userCalloutDismissSpy;
-
- const findUserCalloutDismisser = () => wrapper.findComponent(UserCalloutDismisser);
- const findBanner = () => wrapper.findComponent(GlBanner);
-
- const createComponent = ({ shouldShowCallout = true } = {}) => {
- userCalloutDismissSpy = jest.fn();
-
- wrapper = shallowMount(RegistrationFeedbackBanner, {
- stubs: {
- UserCalloutDismisser: makeMockUserCalloutDismisser({
- dismiss: userCalloutDismissSpy,
- shouldShowCallout,
- }),
- },
- });
- };
-
- it('banner is shown', () => {
- createComponent();
-
- expect(findBanner().exists()).toBe(true);
- });
-
- it('dismisses the callout when closed', () => {
- createComponent();
-
- findBanner().vm.$emit('close');
-
- expect(userCalloutDismissSpy).toHaveBeenCalled();
- });
-
- it('sets feature name to create_runner_workflow_banner', () => {
- createComponent();
-
- expect(findUserCalloutDismisser().props('featureName')).toBe('create_runner_workflow_banner');
- });
-
- it('is not displayed once it has been dismissed', () => {
- createComponent({ shouldShowCallout: false });
-
- expect(findBanner().exists()).toBe(false);
- });
-});
diff --git a/spec/frontend/ci/runner/components/registration/registration_token_spec.js b/spec/frontend/ci/runner/components/registration/registration_token_spec.js
index eccfe43b47f..af9dbe89eb6 100644
--- a/spec/frontend/ci/runner/components/registration/registration_token_spec.js
+++ b/spec/frontend/ci/runner/components/registration/registration_token_spec.js
@@ -55,7 +55,7 @@ describe('RegistrationToken', () => {
mountFn: mountExtended,
});
- expect(wrapper.find('input').element.type).toBe('password');
+ expect(wrapper.find('input').classes()).toContain('input-copy-show-disc');
});
describe('When the copy to clipboard button is clicked', () => {
diff --git a/spec/frontend/ci/runner/components/runner_created_at_spec.js b/spec/frontend/ci/runner/components/runner_created_at_spec.js
new file mode 100644
index 00000000000..19d1e55c72d
--- /dev/null
+++ b/spec/frontend/ci/runner/components/runner_created_at_spec.js
@@ -0,0 +1,97 @@
+import { GlSprintf, GlLink } from '@gitlab/ui';
+import { getIdFromGraphQLId } from '~/graphql_shared/utils';
+import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
+import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+
+import RunnerCreatedAt from '~/ci/runner/components/runner_created_at.vue';
+
+import { runnerData } from '../mock_data';
+
+const mockRunner = runnerData.data.runner;
+
+describe('RunnerCreatedAt', () => {
+ let wrapper;
+
+ const createWrapper = ({ runner = {} } = {}) => {
+ wrapper = mountExtended(RunnerCreatedAt, {
+ propsData: {
+ runner: {
+ ...mockRunner,
+ ...runner,
+ },
+ },
+ stubs: {
+ GlSprintf,
+ TimeAgo,
+ UserAvatarLink,
+ },
+ });
+ };
+
+ const findTimeAgo = () => wrapper.findComponent(TimeAgo);
+ const findLink = () => wrapper.findComponent(GlLink);
+
+ const expectUserLink = (createdBy) => {
+ const { id, name, avatarUrl, webUrl, username } = createdBy;
+
+ expect(findLink().text()).toBe(name);
+ expect(findLink().attributes('href')).toBe(webUrl);
+ expect({ ...findLink().element.dataset }).toEqual({
+ avatarUrl,
+ name,
+ userId: `${getIdFromGraphQLId(id)}`,
+ username,
+ });
+ };
+
+ beforeEach(() => {
+ createWrapper();
+ });
+
+ it('shows creation time and creator', () => {
+ expect(wrapper.text()).toMatchInterpolatedText(
+ `Created by ${mockRunner.createdBy.name} ${findTimeAgo().text()}`,
+ );
+
+ expectUserLink(mockRunner.createdBy);
+ expect(findTimeAgo().props('time')).toBe(mockRunner.createdAt);
+ });
+
+ it('shows creation time with no creator', () => {
+ createWrapper({
+ runner: {
+ createdBy: null,
+ },
+ });
+
+ expect(wrapper.text()).toMatchInterpolatedText(`Created ${findTimeAgo().text()}`);
+
+ expect(findLink().exists()).toBe(false);
+ expect(findTimeAgo().props('time')).toBe(mockRunner.createdAt);
+ });
+
+ it('shows creator with no creation time', () => {
+ createWrapper({
+ runner: {
+ createdAt: null,
+ },
+ });
+
+ expect(wrapper.text()).toMatchInterpolatedText(`Created by ${mockRunner.createdBy.name}`);
+
+ expectUserLink(mockRunner.createdBy);
+ expect(findTimeAgo().exists()).toBe(false);
+ });
+
+ it('shows no creation information', () => {
+ createWrapper({
+ runner: {
+ createdBy: null,
+ createdAt: null,
+ },
+ });
+
+ expect(wrapper.html()).toBe('');
+ });
+});
diff --git a/spec/frontend/ci/runner/components/runner_filtered_search_bar_spec.js b/spec/frontend/ci/runner/components/runner_filtered_search_bar_spec.js
index 7572122a5f3..ffc19d66cac 100644
--- a/spec/frontend/ci/runner/components/runner_filtered_search_bar_spec.js
+++ b/spec/frontend/ci/runner/components/runner_filtered_search_bar_spec.js
@@ -1,4 +1,5 @@
import { GlFilteredSearch, GlDropdown, GlDropdownItem } from '@gitlab/ui';
+import { nextTick } from 'vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { assertProps } from 'helpers/assert_props';
import RunnerFilteredSearchBar from '~/ci/runner/components/runner_filtered_search_bar.vue';
@@ -35,8 +36,8 @@ describe('RunnerList', () => {
const mockOtherSort = CONTACTED_DESC;
const mockFilters = [
- { type: PARAM_KEY_STATUS, value: { data: STATUS_ONLINE, operator: '=' } },
- { type: FILTERED_SEARCH_TERM, value: { data: '' } },
+ { id: 1, type: PARAM_KEY_STATUS, value: { data: STATUS_ONLINE, operator: '=' } },
+ { id: 2, type: FILTERED_SEARCH_TERM, value: { data: '' } },
];
const expectToHaveLastEmittedInput = (value) => {
@@ -148,10 +149,12 @@ describe('RunnerList', () => {
).toEqual('Last contact');
});
- it('when the user sets a filter, the "search" preserves the other filters', () => {
+ it('when the user sets a filter, the "search" preserves the other filters', async () => {
findGlFilteredSearch().vm.$emit('input', mockFilters);
findGlFilteredSearch().vm.$emit('submit');
+ await nextTick();
+
expectToHaveLastEmittedInput({
runnerType: INSTANCE_TYPE,
membership: DEFAULT_MEMBERSHIP,
@@ -162,10 +165,12 @@ describe('RunnerList', () => {
});
});
- it('when the user sets a filter, the "search" is emitted with filters', () => {
+ it('when the user sets a filter, the "search" is emitted with filters', async () => {
findGlFilteredSearch().vm.$emit('input', mockFilters);
findGlFilteredSearch().vm.$emit('submit');
+ await nextTick();
+
expectToHaveLastEmittedInput({
runnerType: null,
membership: DEFAULT_MEMBERSHIP,
diff --git a/spec/frontend/ci/runner/components/runner_header_spec.js b/spec/frontend/ci/runner/components/runner_header_spec.js
index f5091226eaa..c7d95c49d81 100644
--- a/spec/frontend/ci/runner/components/runner_header_spec.js
+++ b/spec/frontend/ci/runner/components/runner_header_spec.js
@@ -11,6 +11,7 @@ import { convertToGraphQLId } from '~/graphql_shared/utils';
import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
import RunnerHeader from '~/ci/runner/components/runner_header.vue';
+import RunnerCreatedAt from '~/ci/runner/components/runner_created_at.vue';
import RunnerTypeBadge from '~/ci/runner/components/runner_type_badge.vue';
import RunnerStatusBadge from '~/ci/runner/components/runner_status_badge.vue';
@@ -25,7 +26,6 @@ describe('RunnerHeader', () => {
const findRunnerTypeBadge = () => wrapper.findComponent(RunnerTypeBadge);
const findRunnerStatusBadge = () => wrapper.findComponent(RunnerStatusBadge);
const findRunnerLockedIcon = () => wrapper.findByTestId('lock-icon');
- const findTimeAgo = () => wrapper.findComponent(TimeAgo);
const createComponent = ({ runner = {}, options = {}, mountFn = shallowMountExtended } = {}) => {
wrapper = mountFn(RunnerHeader, {
@@ -86,24 +86,10 @@ describe('RunnerHeader', () => {
expect(findRunnerLockedIcon().exists()).toBe(true);
});
- it('displays the runner creation time', () => {
+ it('displays the runner creation data', () => {
createComponent();
- expect(wrapper.text()).toMatch(/created .+/);
- expect(findTimeAgo().props('time')).toBe(mockRunner.createdAt);
- });
-
- it('does not display runner creation time if "createdAt" is missing', () => {
- createComponent({
- runner: {
- id: convertToGraphQLId(TYPENAME_CI_RUNNER, 99),
- createdAt: null,
- },
- });
-
- expect(wrapper.text()).toContain(`#99 (${mockRunnerSha})`);
- expect(wrapper.text()).not.toMatch(/created .+/);
- expect(findTimeAgo().exists()).toBe(false);
+ expect(wrapper.findComponent(RunnerCreatedAt).props('runner')).toEqual(mockRunner);
});
it('displays actions in a slot', () => {
diff --git a/spec/frontend/ci/runner/components/runner_list_header_spec.js b/spec/frontend/ci/runner/components/runner_list_header_spec.js
new file mode 100644
index 00000000000..45823fa6813
--- /dev/null
+++ b/spec/frontend/ci/runner/components/runner_list_header_spec.js
@@ -0,0 +1,31 @@
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import RunnerListHeader from '~/ci/runner/components/runner_list_header.vue';
+
+describe('RunnerListHeader', () => {
+ let wrapper;
+ const createWrapper = (options) => {
+ wrapper = shallowMountExtended(RunnerListHeader, {
+ ...options,
+ });
+ };
+
+ it('shows title', () => {
+ createWrapper({
+ scopedSlots: {
+ title: () => 'My title',
+ },
+ });
+
+ expect(wrapper.find('h1').text()).toBe('My title');
+ });
+
+ it('shows actions', () => {
+ createWrapper({
+ scopedSlots: {
+ actions: () => 'My actions',
+ },
+ });
+
+ expect(wrapper.text()).toContain('My actions');
+ });
+});
diff --git a/spec/frontend/ci/runner/components/runner_update_form_spec.js b/spec/frontend/ci/runner/components/runner_update_form_spec.js
index 2ba1c31fe52..a1ecedc6846 100644
--- a/spec/frontend/ci/runner/components/runner_update_form_spec.js
+++ b/spec/frontend/ci/runner/components/runner_update_form_spec.js
@@ -137,7 +137,15 @@ describe('RunnerUpdateForm', () => {
await submitFormAndWait();
// Some read-only fields are not submitted
- const { __typename, shortSha, runnerType, createdAt, status, ...submitted } = mockRunner;
+ const {
+ __typename,
+ shortSha,
+ runnerType,
+ createdAt,
+ createdBy,
+ status,
+ ...submitted
+ } = mockRunner;
expectToHaveSubmittedRunnerContaining(submitted);
});
diff --git a/spec/frontend/ci/runner/group_new_runner_app/group_new_runner_app_spec.js b/spec/frontend/ci/runner/group_new_runner_app/group_new_runner_app_spec.js
index 177fd9bcd9a..623a8f1c5a1 100644
--- a/spec/frontend/ci/runner/group_new_runner_app/group_new_runner_app_spec.js
+++ b/spec/frontend/ci/runner/group_new_runner_app/group_new_runner_app_spec.js
@@ -6,7 +6,6 @@ import { createAlert, VARIANT_SUCCESS } from '~/alert';
import GroupRunnerRunnerApp from '~/ci/runner/group_new_runner/group_new_runner_app.vue';
import RegistrationCompatibilityAlert from '~/ci/runner/components/registration/registration_compatibility_alert.vue';
-import RegistrationFeedbackBanner from '~/ci/runner/components/registration/registration_feedback_banner.vue';
import { saveAlertToLocalStorage } from '~/ci/runner/local_storage_alert/save_alert_to_local_storage';
import RunnerPlatformsRadioGroup from '~/ci/runner/components/runner_platforms_radio_group.vue';
import {
@@ -34,7 +33,6 @@ describe('GroupRunnerRunnerApp', () => {
let wrapper;
const findRunnerPlatformsRadioGroup = () => wrapper.findComponent(RunnerPlatformsRadioGroup);
- const findRegistrationFeedbackBanner = () => wrapper.findComponent(RegistrationFeedbackBanner);
const findRegistrationCompatibilityAlert = () =>
wrapper.findComponent(RegistrationCompatibilityAlert);
const findRunnerCreateForm = () => wrapper.findComponent(RunnerCreateForm);
@@ -54,10 +52,6 @@ describe('GroupRunnerRunnerApp', () => {
createComponent();
});
- it('shows a registration feedback banner', () => {
- expect(findRegistrationFeedbackBanner().exists()).toBe(true);
- });
-
it('shows a registration compatibility alert', () => {
expect(findRegistrationCompatibilityAlert().props('alertKey')).toBe(mockGroupId);
});
diff --git a/spec/frontend/ci/runner/mock_data.js b/spec/frontend/ci/runner/mock_data.js
index b8eb9f0ba1b..51556650c32 100644
--- a/spec/frontend/ci/runner/mock_data.js
+++ b/spec/frontend/ci/runner/mock_data.js
@@ -310,6 +310,23 @@ export const mockSearchExamples = [
first: RUNNER_PAGE_SIZE,
},
},
+ {
+ name: 'version prefix',
+ urlQuery: '?version_prefix[]=16.',
+ search: {
+ runnerType: null,
+ membership: DEFAULT_MEMBERSHIP,
+ filters: [{ type: 'version_prefix', value: { data: '16.', operator: '=' } }],
+ pagination: {},
+ sort: CREATED_DESC,
+ },
+ graphqlVariables: {
+ versionPrefix: '16.',
+ membership: DEFAULT_MEMBERSHIP,
+ sort: CREATED_DESC,
+ first: RUNNER_PAGE_SIZE,
+ },
+ },
];
export const onlineContactTimeoutSecs = 2 * 60 * 60;
diff --git a/spec/frontend/ci/runner/project_new_runner_app/project_new_runner_app_spec.js b/spec/frontend/ci/runner/project_new_runner_app/project_new_runner_app_spec.js
index 22d8e243f7b..3e12f3911a0 100644
--- a/spec/frontend/ci/runner/project_new_runner_app/project_new_runner_app_spec.js
+++ b/spec/frontend/ci/runner/project_new_runner_app/project_new_runner_app_spec.js
@@ -6,7 +6,6 @@ import { createAlert, VARIANT_SUCCESS } from '~/alert';
import ProjectRunnerRunnerApp from '~/ci/runner/project_new_runner/project_new_runner_app.vue';
import RegistrationCompatibilityAlert from '~/ci/runner/components/registration/registration_compatibility_alert.vue';
-import RegistrationFeedbackBanner from '~/ci/runner/components/registration/registration_feedback_banner.vue';
import { saveAlertToLocalStorage } from '~/ci/runner/local_storage_alert/save_alert_to_local_storage';
import RunnerPlatformsRadioGroup from '~/ci/runner/components/runner_platforms_radio_group.vue';
import {
@@ -34,7 +33,6 @@ describe('ProjectRunnerRunnerApp', () => {
let wrapper;
const findRunnerPlatformsRadioGroup = () => wrapper.findComponent(RunnerPlatformsRadioGroup);
- const findRegistrationFeedbackBanner = () => wrapper.findComponent(RegistrationFeedbackBanner);
const findRegistrationCompatibilityAlert = () =>
wrapper.findComponent(RegistrationCompatibilityAlert);
const findRunnerCreateForm = () => wrapper.findComponent(RunnerCreateForm);
@@ -55,10 +53,6 @@ describe('ProjectRunnerRunnerApp', () => {
createComponent();
});
- it('shows a registration feedback banner', () => {
- expect(findRegistrationFeedbackBanner().exists()).toBe(true);
- });
-
it('shows a registration compatibility alert', () => {
expect(findRegistrationCompatibilityAlert().props('alertKey')).toBe(mockProjectId);
});
diff --git a/spec/frontend/ci/runner/runner_edit/runner_edit_app_spec.js b/spec/frontend/ci/runner/runner_edit/runner_edit_app_spec.js
index ee4bd9ccc92..6ceecd79163 100644
--- a/spec/frontend/ci/runner/runner_edit/runner_edit_app_spec.js
+++ b/spec/frontend/ci/runner/runner_edit/runner_edit_app_spec.js
@@ -64,7 +64,7 @@ describe('RunnerEditApp', () => {
await createComponentWithApollo({ mountFn: mount });
expect(findRunnerHeader().text()).toContain(`#${mockRunnerId} (${mockRunnerSha})`);
- expect(findRunnerHeader().text()).toContain('created');
+ expect(findRunnerHeader().text()).toContain('Created');
});
it('displays the runner type and status', async () => {
diff --git a/spec/frontend/ci/runner/sentry_utils_spec.js b/spec/frontend/ci/runner/sentry_utils_spec.js
index 59d386a5899..bb291557288 100644
--- a/spec/frontend/ci/runner/sentry_utils_spec.js
+++ b/spec/frontend/ci/runner/sentry_utils_spec.js
@@ -1,7 +1,7 @@
-import * as Sentry from '@sentry/browser';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import { captureException } from '~/ci/runner/sentry_utils';
-jest.mock('@sentry/browser');
+jest.mock('~/sentry/sentry_browser_wrapper');
describe('~/ci/runner/sentry_utils', () => {
describe('captureException', () => {
diff --git a/spec/frontend/clusters_list/components/clusters_spec.js b/spec/frontend/clusters_list/components/clusters_spec.js
index d4474b1c643..a7ec32c4f32 100644
--- a/spec/frontend/clusters_list/components/clusters_spec.js
+++ b/spec/frontend/clusters_list/components/clusters_spec.js
@@ -1,8 +1,8 @@
import { GlLoadingIcon, GlPagination, GlSkeletonLoader, GlTableLite } from '@gitlab/ui';
-import * as Sentry from '@sentry/browser';
import { mount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import { nextTick } from 'vue';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import Clusters from '~/clusters_list/components/clusters.vue';
import ClustersEmptyState from '~/clusters_list/components/clusters_empty_state.vue';
import ClusterStore from '~/clusters_list/store';
@@ -15,7 +15,7 @@ import {
} from '~/clusters_list/store/mutation_types';
import { apiData } from '../mock_data';
-jest.mock('@sentry/browser');
+jest.mock('~/sentry/sentry_browser_wrapper');
describe('Clusters', () => {
let mock;
diff --git a/spec/frontend/clusters_list/store/actions_spec.js b/spec/frontend/clusters_list/store/actions_spec.js
index 9e6da595a75..fda3fde6baa 100644
--- a/spec/frontend/clusters_list/store/actions_spec.js
+++ b/spec/frontend/clusters_list/store/actions_spec.js
@@ -1,5 +1,5 @@
-import * as Sentry from '@sentry/browser';
import MockAdapter from 'axios-mock-adapter';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import testAction from 'helpers/vuex_action_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { MAX_REQUESTS } from '~/clusters_list/constants';
diff --git a/spec/frontend/commit/components/commit_box_pipeline_status_spec.js b/spec/frontend/commit/components/commit_box_pipeline_status_spec.js
index 844a2d81832..008a1b2c068 100644
--- a/spec/frontend/commit/components/commit_box_pipeline_status_spec.js
+++ b/spec/frontend/commit/components/commit_box_pipeline_status_spec.js
@@ -5,7 +5,7 @@ import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { createAlert } from '~/alert';
-import CiBadgeLink from '~/vue_shared/components/ci_badge_link.vue';
+import CiIcon from '~/vue_shared/components/ci_icon.vue';
import CommitBoxPipelineStatus from '~/projects/commit_box/info/components/commit_box_pipeline_status.vue';
import {
COMMIT_BOX_POLL_INTERVAL,
@@ -32,7 +32,7 @@ describe('Commit box pipeline status', () => {
const failedHandler = jest.fn().mockRejectedValue(new Error('GraphQL error'));
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
- const findCiBadgeLink = () => wrapper.findComponent(CiBadgeLink);
+ const findCiIcon = () => wrapper.findComponent(CiIcon);
const advanceToNextFetch = () => {
jest.advanceTimersByTime(COMMIT_BOX_POLL_INTERVAL);
@@ -50,7 +50,7 @@ describe('Commit box pipeline status', () => {
...mockProvide,
},
stubs: {
- CiBadgeLink,
+ CiIcon,
},
apolloProvider: createMockApolloProvider(handler),
});
@@ -61,7 +61,7 @@ describe('Commit box pipeline status', () => {
createComponent();
expect(findLoadingIcon().exists()).toBe(true);
- expect(findCiBadgeLink().exists()).toBe(false);
+ expect(findCiIcon().exists()).toBe(false);
});
});
@@ -73,7 +73,7 @@ describe('Commit box pipeline status', () => {
});
it('should display pipeline status after the query is resolved successfully', () => {
- expect(findCiBadgeLink().exists()).toBe(true);
+ expect(findCiIcon().exists()).toBe(true);
expect(findLoadingIcon().exists()).toBe(false);
expect(createAlert).toHaveBeenCalledTimes(0);
@@ -90,7 +90,7 @@ describe('Commit box pipeline status', () => {
},
} = mockPipelineStatusResponse;
- expect(findCiBadgeLink().attributes('href')).toBe(detailsPath);
+ expect(findCiIcon().attributes('href')).toBe(detailsPath);
});
});
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 fc8460c7f84..6f0c0ee6ed5 100644
--- a/spec/frontend/content_editor/extensions/code_block_highlight_spec.js
+++ b/spec/frontend/content_editor/extensions/code_block_highlight_spec.js
@@ -61,6 +61,18 @@ describe('content_editor/extensions/code_block_highlight', () => {
expect(editorHtmlOutput.classList.toString()).toContain('content-editor-code-block');
});
+
+ it('includes the lowlight plugin', () => {
+ expect(tiptapEditor.state.plugins).toContainEqual(
+ expect.objectContaining({ key: expect.stringContaining('lowlight') }),
+ );
+ });
+
+ it('does not include the VSCode paste plugin', () => {
+ expect(tiptapEditor.state.plugins).not.toContainEqual(
+ expect.objectContaining({ key: expect.stringContaining('VSCode') }),
+ );
+ });
});
describe.each`
diff --git a/spec/frontend/content_editor/extensions/copy_paste_spec.js b/spec/frontend/content_editor/extensions/copy_paste_spec.js
index 4729b1c1223..e290b4e5137 100644
--- a/spec/frontend/content_editor/extensions/copy_paste_spec.js
+++ b/spec/frontend/content_editor/extensions/copy_paste_spec.js
@@ -1,24 +1,24 @@
import CopyPaste from '~/content_editor/extensions/copy_paste';
import CodeBlockHighlight from '~/content_editor/extensions/code_block_highlight';
-import Loading from '~/content_editor/extensions/loading';
+import Loading, { findAllLoaders } from '~/content_editor/extensions/loading';
import Diagram from '~/content_editor/extensions/diagram';
import Frontmatter from '~/content_editor/extensions/frontmatter';
+import Selection from '~/content_editor/extensions/selection';
import Heading from '~/content_editor/extensions/heading';
import Bold from '~/content_editor/extensions/bold';
import BulletList from '~/content_editor/extensions/bullet_list';
import ListItem from '~/content_editor/extensions/list_item';
import Italic from '~/content_editor/extensions/italic';
+import Table from '~/content_editor/extensions/table';
+import TableCell from '~/content_editor/extensions/table_cell';
+import TableRow from '~/content_editor/extensions/table_row';
+import TableHeader from '~/content_editor/extensions/table_header';
import { VARIANT_DANGER } from '~/alert';
import eventHubFactory from '~/helpers/event_hub_factory';
import { ALERT_EVENT } from '~/content_editor/constants';
import waitForPromises from 'helpers/wait_for_promises';
import MarkdownSerializer from '~/content_editor/services/markdown_serializer';
-import {
- createTestEditor,
- createDocBuilder,
- waitUntilNextDocTransaction,
- sleep,
-} from '../test_utils';
+import { createTestEditor, createDocBuilder, waitUntilNextDocTransaction } from '../test_utils';
const CODE_BLOCK_HTML = '<pre class="js-syntax-highlight" lang="javascript">var a = 2;</pre>';
const CODE_SUGGESTION_HTML =
@@ -35,13 +35,11 @@ describe('content_editor/extensions/copy_paste', () => {
let p;
let bold;
let italic;
- let loading;
let heading;
let codeBlock;
let bulletList;
let listItem;
let renderMarkdown;
- let resolveRenderMarkdownPromise;
let resolveRenderMarkdownPromiseAndWait;
let eventHub;
@@ -52,7 +50,6 @@ describe('content_editor/extensions/copy_paste', () => {
renderMarkdown = jest.fn().mockImplementation(
() =>
new Promise((resolve) => {
- resolveRenderMarkdownPromise = resolve;
resolveRenderMarkdownPromiseAndWait = (data) =>
waitUntilNextDocTransaction({ tiptapEditor, action: () => resolve(data) });
}),
@@ -65,28 +62,34 @@ describe('content_editor/extensions/copy_paste', () => {
Bold,
Italic,
Loading,
+ Selection,
CodeBlockHighlight,
Diagram,
Frontmatter,
Heading,
BulletList,
ListItem,
+ Table,
+ TableCell,
+ TableRow,
+ TableHeader,
CopyPaste.configure({ renderMarkdown, eventHub, serializer: new MarkdownSerializer() }),
],
});
({
- builders: { doc, p, bold, italic, heading, loading, codeBlock, bulletList, listItem },
+ builders: { doc, p, bold, italic, heading, codeBlock, bulletList, listItem },
} = createDocBuilder({
tiptapEditor,
names: {
bold: { markType: Bold.name },
italic: { markType: Italic.name },
- loading: { nodeType: Loading.name },
heading: { nodeType: Heading.name },
bulletList: { nodeType: BulletList.name },
listItem: { nodeType: ListItem.name },
codeBlock: { nodeType: CodeBlockHighlight.name },
+ diagram: { nodeType: Diagram.name },
+ frontmatter: { nodeType: Frontmatter.name },
},
}));
});
@@ -110,17 +113,6 @@ describe('content_editor/extensions/copy_paste', () => {
});
};
- const triggerPasteEventHandlerAndWaitForTransaction = (event) => {
- return waitUntilNextDocTransaction({
- tiptapEditor,
- action: () => {
- tiptapEditor.view.someProp('handlePaste', (eventHandler) => {
- return eventHandler(tiptapEditor.view, event);
- });
- },
- });
- };
-
it.each`
types | data | formatDesc
${['text/plain']} | ${{}} | ${'plain text'}
@@ -183,37 +175,57 @@ describe('content_editor/extensions/copy_paste', () => {
});
});
+ describe('when copying content with a single table cell', () => {
+ it('sets the clipboard data properly', () => {
+ const event = buildClipboardEvent({ eventName: 'copy' });
+
+ tiptapEditor.commands.insertContent('<table><tr><td>Cell 1</td></tr></table>');
+ tiptapEditor.commands.selectAll();
+ tiptapEditor.view.dispatchEvent(event);
+
+ expect(event.clipboardData.setData).toHaveBeenCalledWith('text/x-gfm', 'Cell 1');
+ });
+ });
+
+ describe('when copying content with a table with multiple cells', () => {
+ it('sets the clipboard data properly', () => {
+ const event = buildClipboardEvent({ eventName: 'copy' });
+
+ tiptapEditor.commands.insertContent('<table><tr><td>Cell 1</td><td>Cell 2</td></tr></table>');
+ tiptapEditor.commands.selectAll();
+ tiptapEditor.view.dispatchEvent(event);
+
+ expect(event.clipboardData.setData).toHaveBeenCalledWith(
+ 'text/x-gfm',
+ `<table>
+<tr>
+<td>Cell 1</td>
+<td>Cell 2</td>
+</tr>
+</table>
+
+`,
+ );
+ });
+ });
+
describe('when pasting raw markdown source', () => {
it('shows a loading indicator while markdown is being processed', async () => {
- const expectedDoc = doc(p(loading({ id: expect.any(String) })));
-
- await triggerPasteEventHandlerAndWaitForTransaction(buildClipboardEvent());
+ await triggerPasteEventHandler(buildClipboardEvent());
- expect(tiptapEditor.state.doc.toJSON()).toEqual(expectedDoc.toJSON());
+ expect(findAllLoaders(tiptapEditor.state)).toHaveLength(1);
});
it('pastes in the correct position if some content is added before the markdown is processed', async () => {
const expectedDoc = doc(p(bold('some markdown'), 'some content'));
const resolvedValue = '<strong>some markdown</strong>';
- await triggerPasteEventHandlerAndWaitForTransaction(buildClipboardEvent());
+ await triggerPasteEventHandler(buildClipboardEvent());
tiptapEditor.commands.insertContent('some content');
- await resolveRenderMarkdownPromiseAndWait(resolvedValue);
-
- expect(tiptapEditor.state.doc.toJSON()).toEqual(expectedDoc.toJSON());
- expect(tiptapEditor.state.selection.from).toEqual(26); // end of the document
- });
-
- it('does not paste anything if the loading indicator is deleted before the markdown is processed', async () => {
- const expectedDoc = doc(p());
- await triggerPasteEventHandlerAndWaitForTransaction(buildClipboardEvent());
- tiptapEditor.chain().selectAll().deleteSelection().run();
- resolveRenderMarkdownPromise('some markdown');
+ await resolveRenderMarkdownPromiseAndWait(resolvedValue);
- // wait some time to be sure no transaction happened
- await sleep();
expect(tiptapEditor.state.doc.toJSON()).toEqual(expectedDoc.toJSON());
});
@@ -227,7 +239,7 @@ describe('content_editor/extensions/copy_paste', () => {
it('transforms pasted text into a prosemirror node', async () => {
const expectedDoc = doc(p(bold('bold text')));
- await triggerPasteEventHandlerAndWaitForTransaction(buildClipboardEvent());
+ await triggerPasteEventHandler(buildClipboardEvent());
await resolveRenderMarkdownPromiseAndWait(resolvedValue);
expect(tiptapEditor.state.doc.toJSON()).toEqual(expectedDoc.toJSON());
@@ -239,7 +251,7 @@ describe('content_editor/extensions/copy_paste', () => {
tiptapEditor.commands.setContent('Initial text and ');
- await triggerPasteEventHandlerAndWaitForTransaction(buildClipboardEvent());
+ await triggerPasteEventHandler(buildClipboardEvent());
await resolveRenderMarkdownPromiseAndWait(resolvedValue);
expect(tiptapEditor.state.doc.toJSON()).toEqual(expectedDoc.toJSON());
@@ -253,7 +265,7 @@ describe('content_editor/extensions/copy_paste', () => {
tiptapEditor.commands.setContent('Initial text and ');
tiptapEditor.commands.setTextSelection({ from: 13, to: 17 });
- await triggerPasteEventHandlerAndWaitForTransaction(buildClipboardEvent());
+ await triggerPasteEventHandler(buildClipboardEvent());
await resolveRenderMarkdownPromiseAndWait(resolvedValue);
expect(tiptapEditor.state.doc.toJSON()).toEqual(expectedDoc.toJSON());
@@ -274,7 +286,7 @@ describe('content_editor/extensions/copy_paste', () => {
tiptapEditor.commands.setContent('Initial text and ');
- await triggerPasteEventHandlerAndWaitForTransaction(buildClipboardEvent());
+ await triggerPasteEventHandler(buildClipboardEvent());
await resolveRenderMarkdownPromiseAndWait(resolvedValue);
expect(tiptapEditor.state.doc.toJSON()).toEqual(expectedDoc.toJSON());
@@ -289,7 +301,7 @@ describe('content_editor/extensions/copy_paste', () => {
const expectedDoc = doc(p(bold('bold text')), p('some code'));
- await triggerPasteEventHandlerAndWaitForTransaction(
+ await triggerPasteEventHandler(
buildClipboardEvent({
types: ['text/html'],
data: {
@@ -309,7 +321,7 @@ describe('content_editor/extensions/copy_paste', () => {
const resolvedValue = '<strong>bold text</strong>';
const expectedDoc = doc(p(bold('bold text')));
- await triggerPasteEventHandlerAndWaitForTransaction(
+ await triggerPasteEventHandler(
buildClipboardEvent({
types: ['text/x-gfm', 'text/plain', 'text/html'],
data: {
@@ -332,7 +344,7 @@ describe('content_editor/extensions/copy_paste', () => {
bulletList(listItem(p('Cat')), listItem(p('Dog')), listItem(p('Turtle'))),
);
- await triggerPasteEventHandlerAndWaitForTransaction(
+ await triggerPasteEventHandler(
buildClipboardEvent({
types: ['text/plain', 'text/html'],
data: {
@@ -359,7 +371,7 @@ describe('content_editor/extensions/copy_paste', () => {
),
);
- await triggerPasteEventHandlerAndWaitForTransaction(
+ await triggerPasteEventHandler(
buildClipboardEvent({
types: ['vscode-editor-data', 'text/plain', 'text/html'],
data: {
@@ -380,7 +392,7 @@ describe('content_editor/extensions/copy_paste', () => {
const expectedDoc = doc(p(bold('bold text')));
- await triggerPasteEventHandlerAndWaitForTransaction(
+ await triggerPasteEventHandler(
buildClipboardEvent({
types: ['vscode-editor-data', 'text/plain', 'text/html'],
data: {
diff --git a/spec/frontend/content_editor/extensions/horizontal_rule_spec.js b/spec/frontend/content_editor/extensions/horizontal_rule_spec.js
index 322c04a42e1..14241ae9cc6 100644
--- a/spec/frontend/content_editor/extensions/horizontal_rule_spec.js
+++ b/spec/frontend/content_editor/extensions/horizontal_rule_spec.js
@@ -22,7 +22,7 @@ describe('content_editor/extensions/horizontal_rule', () => {
it.each`
input | insertedNodes
- ${'---'} | ${() => [p(), horizontalRule()]}
+ ${'---'} | ${() => [horizontalRule(), p()]}
${'--'} | ${() => [p()]}
${'---x'} | ${() => [p()]}
${' ---x'} | ${() => [p()]}
diff --git a/spec/frontend/content_editor/extensions/html_marks_spec.js b/spec/frontend/content_editor/extensions/html_marks_spec.js
new file mode 100644
index 00000000000..3757962ce52
--- /dev/null
+++ b/spec/frontend/content_editor/extensions/html_marks_spec.js
@@ -0,0 +1,89 @@
+import HTMLMarks from '~/content_editor/extensions/html_marks';
+import { createTestEditor, createDocBuilder } from '../test_utils';
+
+describe('content_editor/extensions/html_marks', () => {
+ let tiptapEditor;
+ let doc;
+ let ins;
+ let abbr;
+ let bdo;
+ let cite;
+ let dfn;
+ let small;
+ let span;
+ let time;
+ let kbd;
+ let q;
+ let p;
+ let samp;
+ let varMark;
+ let ruby;
+ let rp;
+ let rt;
+
+ beforeEach(() => {
+ tiptapEditor = createTestEditor({ extensions: [...HTMLMarks] });
+
+ ({
+ builders: {
+ doc,
+ ins,
+ abbr,
+ bdo,
+ cite,
+ dfn,
+ small,
+ span,
+ time,
+ kbd,
+ q,
+ samp,
+ var: varMark,
+ ruby,
+ rp,
+ rt,
+ p,
+ },
+ } = createDocBuilder({
+ tiptapEditor,
+ names: {
+ ...HTMLMarks.reduce(
+ (builders, htmlMark) => ({
+ ...builders,
+ [htmlMark.name]: { markType: htmlMark.name },
+ }),
+ {},
+ ),
+ },
+ }));
+ });
+
+ it.each`
+ input | expectedContent
+ ${'<ins>inserted</ins>'} | ${() => ins('inserted')}
+ ${'<abbr title="abbr">abbreviation</abbr>'} | ${() => abbr({ title: 'abbr' }, 'abbreviation')}
+ ${'<bdo dir="rtl">bdo</bdo>'} | ${() => bdo({ dir: 'rtl' }, 'bdo')}
+ ${'<cite>citation</cite>'} | ${() => cite('citation')}
+ ${'<dfn>definition</dfn>'} | ${() => dfn('definition')}
+ ${'<small>small text</small>'} | ${() => small('small text')}
+ ${'<span dir="rtl">span text</span>'} | ${() => span({ dir: 'rtl' }, 'span text')}
+ ${'<time datetime="2023-11-02">November 2, 2023</time>'} | ${() => time({ datetime: '2023-11-02' }, 'November 2, 2023')}
+ ${'<kbd>keyboard</kbd>'} | ${() => kbd('keyboard')}
+ ${'<q>quote</q>'} | ${() => q('quote')}
+ ${'<samp>sample</samp>'} | ${() => samp('sample')}
+ ${'<var>variable</var>'} | ${() => varMark('variable')}
+ ${'<ruby>base<rp>(</rp><rt>ruby</rt><rp>)</rp></ruby>'} | ${() => ruby('base', rp('('), rt('ruby'), rp(')'))}
+ `('parses and creates marks for $input', ({ input, expectedContent }) => {
+ tiptapEditor.commands.setContent(input);
+ expect(tiptapEditor.getJSON()).toEqual(doc(p(expectedContent())).toJSON());
+ expect(tiptapEditor.getHTML()).toContain(input);
+ });
+
+ it('does not parse an element with a data-escaped-char attribute', () => {
+ const input = '<span data-escaped-char>#</span> not a heading';
+ const expectedDoc = doc(p('# not a heading'));
+ tiptapEditor.commands.setContent(input);
+ expect(tiptapEditor.getJSON()).toEqual(expectedDoc.toJSON());
+ expect(tiptapEditor.getHTML()).not.toContain('<span');
+ });
+});
diff --git a/spec/frontend/content_editor/extensions/word_break_spec.js b/spec/frontend/content_editor/extensions/word_break_spec.js
index 23167269d7d..5caca218702 100644
--- a/spec/frontend/content_editor/extensions/word_break_spec.js
+++ b/spec/frontend/content_editor/extensions/word_break_spec.js
@@ -22,11 +22,11 @@ describe('content_editor/extensions/word_break', () => {
it.each`
input | insertedNode
- ${'<wbr>'} | ${() => p(wordBreak())}
- ${'<wbr'} | ${() => p()}
- ${'wbr>'} | ${() => p()}
+ ${'<wbr>'} | ${() => [p(wordBreak()), p()]}
+ ${'<wbr'} | ${() => [p()]}
+ ${'wbr>'} | ${() => [p()]}
`('with input=$input, then should insert a $insertedNode', ({ input, insertedNode }) => {
- const expectedDoc = doc(insertedNode());
+ const expectedDoc = doc(...insertedNode());
triggerNodeInputRule({ tiptapEditor, inputRuleText: input });
diff --git a/spec/frontend/contribution_events/components/contribution_event/contribution_event_base_spec.js b/spec/frontend/contribution_events/components/contribution_event/contribution_event_base_spec.js
index 310966243d1..f5850401b8e 100644
--- a/spec/frontend/contribution_events/components/contribution_event/contribution_event_base_spec.js
+++ b/spec/frontend/contribution_events/components/contribution_event/contribution_event_base_spec.js
@@ -38,7 +38,7 @@ describe('ContributionEventBase', () => {
expect(avatarLink.attributes('href')).toBe(defaultPropsData.event.author.web_url);
expect(avatarLabeled.attributes()).toMatchObject({
src: defaultPropsData.event.author.avatar_url,
- size: '32',
+ size: '24',
});
expect(avatarLabeled.props()).toMatchObject({
label: defaultPropsData.event.author.name,
diff --git a/spec/frontend/crm/organization_form_wrapper_spec.js b/spec/frontend/crm/organization_form_wrapper_spec.js
index f15fcac71d5..4e84180b22b 100644
--- a/spec/frontend/crm/organization_form_wrapper_spec.js
+++ b/spec/frontend/crm/organization_form_wrapper_spec.js
@@ -2,8 +2,8 @@ import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import OrganizationFormWrapper from '~/crm/organizations/components/organization_form_wrapper.vue';
import CrmForm from '~/crm/components/crm_form.vue';
import getGroupOrganizationsQuery from '~/crm/organizations/components/graphql/get_group_organizations.query.graphql';
-import createOrganizationMutation from '~/crm/organizations/components/graphql/create_customer_relations_organization.mutation.graphql';
-import updateOrganizationMutation from '~/crm/organizations/components/graphql/update_organization.mutation.graphql';
+import createCustomerRelationsOrganizationMutation from '~/crm/organizations/components/graphql/create_customer_relations_organization.mutation.graphql';
+import updateCustomerRelationsOrganizationMutation from '~/crm/organizations/components/graphql/update_customer_relations_organization.mutation.graphql';
describe('Customer relations organization form wrapper', () => {
let wrapper;
@@ -48,7 +48,7 @@ describe('Customer relations organization form wrapper', () => {
expect(organizationForm.props('fields')).toHaveLength(4);
expect(organizationForm.props('title')).toBe('Edit organization');
expect(organizationForm.props('successMessage')).toBe('Organization has been updated.');
- expect(organizationForm.props('mutation')).toBe(updateOrganizationMutation);
+ expect(organizationForm.props('mutation')).toBe(updateCustomerRelationsOrganizationMutation);
expect(organizationForm.props('getQuery')).toMatchObject({
query: getGroupOrganizationsQuery,
variables: { groupFullPath: 'flightjs' },
@@ -69,7 +69,7 @@ describe('Customer relations organization form wrapper', () => {
expect(organizationForm.props('fields')).toHaveLength(3);
expect(organizationForm.props('title')).toBe('New organization');
expect(organizationForm.props('successMessage')).toBe('Organization has been added.');
- expect(organizationForm.props('mutation')).toBe(createOrganizationMutation);
+ expect(organizationForm.props('mutation')).toBe(createCustomerRelationsOrganizationMutation);
expect(organizationForm.props('getQuery')).toMatchObject({
query: getGroupOrganizationsQuery,
variables: { groupFullPath: 'flightjs' },
diff --git a/spec/frontend/custom_emoji/components/delete_item_spec.js b/spec/frontend/custom_emoji/components/delete_item_spec.js
index 06c4ca8d54b..2d5594b8a65 100644
--- a/spec/frontend/custom_emoji/components/delete_item_spec.js
+++ b/spec/frontend/custom_emoji/components/delete_item_spec.js
@@ -1,7 +1,7 @@
import Vue from 'vue';
import VueApollo from 'vue-apollo';
-import * as Sentry from '@sentry/browser';
import { GlModal } from '@gitlab/ui';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
@@ -11,7 +11,7 @@ import deleteCustomEmojiMutation from '~/custom_emoji/queries/delete_custom_emoj
import { CUSTOM_EMOJI } from '../mock_data';
jest.mock('~/alert');
-jest.mock('@sentry/browser');
+jest.mock('~/sentry/sentry_browser_wrapper');
let wrapper;
let deleteMutationSpy;
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
index 8bc9bce7e80..f77f1a791f9 100644
--- 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
@@ -2,7 +2,7 @@
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"
+ class="disabled-comment gl-text-center gl-text-secondary"
>
Please
<gl-link-stub
@@ -22,7 +22,7 @@ exports[`DesignNoteSignedOut renders message containing register and sign-in lin
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"
+ class="disabled-comment gl-text-center gl-text-secondary"
>
Please
<gl-link-stub
diff --git a/spec/frontend/design_management/components/design_notes/__snapshots__/design_reply_form_spec.js.snap b/spec/frontend/design_management/components/design_notes/__snapshots__/design_reply_form_spec.js.snap
index 206187c3530..696b04868f7 100644
--- a/spec/frontend/design_management/components/design_notes/__snapshots__/design_reply_form_spec.js.snap
+++ b/spec/frontend/design_management/components/design_notes/__snapshots__/design_reply_form_spec.js.snap
@@ -3,7 +3,7 @@
exports[`Design reply form component renders button text as "Comment" when creating a comment 1`] = `
<button
class="btn btn-confirm btn-md disabled gl-button gl-mr-3 gl-w-auto!"
- data-qa-selector="save_comment_button"
+ data-testid="save-comment-button"
data-track-action="click_button"
disabled=""
type="submit"
@@ -19,7 +19,7 @@ exports[`Design reply form component renders button text as "Comment" when creat
exports[`Design reply form component renders button text as "Save comment" when creating a comment 1`] = `
<button
class="btn btn-confirm btn-md disabled gl-button gl-mr-3 gl-w-auto!"
- data-qa-selector="save_comment_button"
+ data-testid="save-comment-button"
data-track-action="click_button"
disabled=""
type="submit"
diff --git a/spec/frontend/design_management/components/design_notes/design_note_spec.js b/spec/frontend/design_management/components/design_notes/design_note_spec.js
index 8795b089551..28b264cede9 100644
--- a/spec/frontend/design_management/components/design_notes/design_note_spec.js
+++ b/spec/frontend/design_management/components/design_notes/design_note_spec.js
@@ -1,7 +1,7 @@
import { ApolloMutation } from 'vue-apollo';
import { nextTick } from 'vue';
import { GlAvatar, GlAvatarLink, GlDisclosureDropdown, GlDisclosureDropdownItem } from '@gitlab/ui';
-import * as Sentry from '@sentry/browser';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
@@ -51,13 +51,19 @@ describe('Design note component', () => {
const findDropdown = () => wrapper.findComponent(GlDisclosureDropdown);
const findDropdownItems = () => findDropdown().findAllComponents(GlDisclosureDropdownItem);
const findEditDropdownItem = () => findDropdownItems().at(0);
- const findDeleteDropdownItem = () => findDropdownItems().at(1);
+ const findCopyLinkDropdownItem = () => findDropdownItems().at(1);
+ const findDeleteDropdownItem = () => findDropdownItems().at(2);
+
+ const showToast = jest.fn();
function createComponent({
props = {},
data = { isEditing: false },
mountFn = mountExtended,
mocks = {
+ $toast: {
+ show: showToast,
+ },
$route,
$apollo: {
mutate: jest.fn().mockResolvedValue({ data: { updateNote: {} } }),
@@ -239,6 +245,7 @@ describe('Design note component', () => {
expect(findDropdown().exists()).toBe(true);
expect(findEditDropdownItem().exists()).toBe(true);
+ expect(findCopyLinkDropdownItem().exists()).toBe(true);
expect(findDeleteDropdownItem().exists()).toBe(true);
expect(findDropdown().props('items')[0].extraAttrs.class).toBe('gl-sm-display-none!');
});
@@ -266,6 +273,40 @@ describe('Design note component', () => {
expect(wrapper.emitted()).toEqual({ 'delete-note': [[{ ...payload }]] });
});
+ it('shows a success toast after copying the url to the clipboard', () => {
+ createComponent({
+ props: {
+ note: {
+ ...note,
+ userPermissions: {
+ adminNote: true,
+ },
+ },
+ },
+ });
+
+ findCopyLinkDropdownItem().find('button').trigger('click');
+
+ expect(showToast).toHaveBeenCalledWith('Link copied to clipboard.');
+ });
+
+ it('has data-clipboard-text set to the correct url', () => {
+ createComponent({
+ props: {
+ note: {
+ ...note,
+ userPermissions: {
+ adminNote: true,
+ },
+ },
+ },
+ });
+
+ expect(findCopyLinkDropdownItem().props('item').extraAttrs['data-clipboard-text']).toBe(
+ 'http://test.host/#note_123',
+ );
+ });
+
describe('when user has award emoji permissions', () => {
const findEmojiPicker = () => wrapper.findComponent(EmojiPicker);
const propsData = {
diff --git a/spec/frontend/design_management/components/design_overlay_spec.js b/spec/frontend/design_management/components/design_overlay_spec.js
index 3eb47fdb97e..6b08b44c39e 100644
--- a/spec/frontend/design_management/components/design_overlay_spec.js
+++ b/spec/frontend/design_management/components/design_overlay_spec.js
@@ -86,7 +86,7 @@ describe('Design overlay component', () => {
};
wrapper
- .find('[data-qa-selector="design_image_button"]')
+ .find('[data-testid="design-image-button"]')
.trigger('mouseup', { offsetX: newCoordinates.x, offsetY: newCoordinates.y });
expect(wrapper.emitted('openCommentForm')).toEqual([
diff --git a/spec/frontend/design_management/components/design_presentation_spec.js b/spec/frontend/design_management/components/design_presentation_spec.js
index e64dec14461..96c2b4da2c6 100644
--- a/spec/frontend/design_management/components/design_presentation_spec.js
+++ b/spec/frontend/design_management/components/design_presentation_spec.js
@@ -46,7 +46,7 @@ describe('Design management design presentation component', () => {
wrapper.element.scrollTo = jest.fn();
}
- const findOverlayCommentButton = () => wrapper.find('[data-qa-selector="design_image_button"]');
+ const findOverlayCommentButton = () => wrapper.find('[data-testid="design-image-button"]');
/**
* Spy on $refs and mock given values
diff --git a/spec/frontend/design_management/components/list/__snapshots__/item_spec.js.snap b/spec/frontend/design_management/components/list/__snapshots__/item_spec.js.snap
index 53359b02b4c..a05b3baecd3 100644
--- a/spec/frontend/design_management/components/list/__snapshots__/item_spec.js.snap
+++ b/spec/frontend/design_management/components/list/__snapshots__/item_spec.js.snap
@@ -17,12 +17,12 @@ exports[`Design management list item component with notes renders item with mult
>
<gl-intersection-observer-stub
class="gl-flex-grow-1"
+ data-qa-filename="test"
+ data-testid="design-image"
>
<img
alt="test"
class="design-img gl-display-block gl-max-h-full gl-max-w-full gl-mx-auto gl-w-auto"
- data-qa-filename="test"
- data-qa-selector="design_image"
data-testid="design-img-1"
src="null"
/>
@@ -33,10 +33,10 @@ exports[`Design management list item component with notes renders item with mult
>
<div
class="gl-display-flex gl-flex-direction-column str-truncated-100"
+ data-testid="design-file-name"
>
<span
class="gl-font-weight-semibold str-truncated-100"
- data-qa-selector="design_file_name"
data-testid="design-img-filename-1"
title="test"
>
@@ -82,12 +82,12 @@ exports[`Design management list item component with notes renders item with sing
>
<gl-intersection-observer-stub
class="gl-flex-grow-1"
+ data-qa-filename="test"
+ data-testid="design-image"
>
<img
alt="test"
class="design-img gl-display-block gl-max-h-full gl-max-w-full gl-mx-auto gl-w-auto"
- data-qa-filename="test"
- data-qa-selector="design_image"
data-testid="design-img-1"
src="null"
/>
@@ -98,10 +98,10 @@ exports[`Design management list item component with notes renders item with sing
>
<div
class="gl-display-flex gl-flex-direction-column str-truncated-100"
+ data-testid="design-file-name"
>
<span
class="gl-font-weight-semibold str-truncated-100"
- data-qa-selector="design_file_name"
data-testid="design-img-filename-1"
title="test"
>
diff --git a/spec/frontend/diffs/components/app_spec.js b/spec/frontend/diffs/components/app_spec.js
index 212def72b90..63d9a2471b6 100644
--- a/spec/frontend/diffs/components/app_spec.js
+++ b/spec/frontend/diffs/components/app_spec.js
@@ -2,8 +2,11 @@ import { GlLoadingIcon, GlPagination } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import Vue, { nextTick } from 'vue';
+import VueApollo from 'vue-apollo';
// eslint-disable-next-line no-restricted-imports
import Vuex from 'vuex';
+import getMRCodequalityAndSecurityReports from '~/diffs/components/graphql/get_mr_codequality_and_security_reports.query.graphql';
+import createMockApollo from 'helpers/mock_apollo_helper';
import setWindowLocation from 'helpers/set_window_location_helper';
import { TEST_HOST } from 'spec/test_constants';
@@ -19,11 +22,14 @@ import CollapsedFilesWarning from '~/diffs/components/collapsed_files_warning.vu
import HiddenFilesWarning from '~/diffs/components/hidden_files_warning.vue';
import eventHub from '~/diffs/event_hub';
+import { EVT_DISCUSSIONS_ASSIGNED } from '~/diffs/constants';
import axios from '~/lib/utils/axios_utils';
import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
import { Mousetrap } from '~/lib/mousetrap';
import * as urlUtils from '~/lib/utils/url_utility';
+import * as commonUtils from '~/lib/utils/common_utils';
+import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
import { stubPerformanceWebAPI } from 'helpers/performance';
import createDiffsStore from '../create_diffs_store';
import diffsMockData from '../mock_data/merge_request_diffs';
@@ -34,6 +40,7 @@ const COMMIT_URL = `${TEST_HOST}/COMMIT/OLD`;
const UPDATED_COMMIT_URL = `${TEST_HOST}/COMMIT/NEW`;
Vue.use(Vuex);
+Vue.use(VueApollo);
Vue.config.ignoredElements = ['copy-code'];
@@ -46,12 +53,19 @@ describe('diffs/components/app', () => {
let store;
let wrapper;
let mock;
+ let fakeApollo;
+
+ const codeQualityAndSastQueryHandlerSuccess = jest.fn().mockResolvedValue({});
function createComponent(props = {}, extendStore = () => {}, provisions = {}, baseConfig = {}) {
+ fakeApollo = createMockApollo([
+ [getMRCodequalityAndSecurityReports, codeQualityAndSastQueryHandlerSuccess],
+ ]);
+
const provide = {
...provisions,
glFeatures: {
- ...(provisions.glFeatures || {}),
+ ...provisions.glFeatures,
},
};
@@ -74,10 +88,11 @@ describe('diffs/components/app', () => {
});
wrapper = shallowMount(App, {
+ apolloProvider: fakeApollo,
propsData: {
endpointCoverage: `${TEST_HOST}/diff/endpointCoverage`,
endpointCodequality: '',
- endpointSast: '',
+ sastReportAvailable: false,
projectPath: 'namespace/project',
currentUser: {},
changesEmptyStateIllustration: '',
@@ -120,8 +135,6 @@ describe('diffs/components/app', () => {
jest.spyOn(wrapper.vm, 'fetchDiffFilesBatch').mockImplementation(fetchResolver);
jest.spyOn(wrapper.vm, 'fetchCoverageFiles').mockImplementation(fetchResolver);
jest.spyOn(wrapper.vm, 'setDiscussions').mockImplementation(() => {});
- jest.spyOn(wrapper.vm, 'unwatchDiscussions').mockImplementation(() => {});
- jest.spyOn(wrapper.vm, 'unwatchRetrievingBatches').mockImplementation(() => {});
store.state.diffs.retrievingBatches = true;
store.state.diffs.diffFiles = [];
return nextTick();
@@ -136,9 +149,7 @@ describe('diffs/components/app', () => {
expect(wrapper.vm.fetchDiffFilesMeta).toHaveBeenCalled();
expect(wrapper.vm.fetchDiffFilesBatch).toHaveBeenCalled();
expect(wrapper.vm.fetchCoverageFiles).toHaveBeenCalled();
- expect(wrapper.vm.unwatchDiscussions).toHaveBeenCalled();
expect(wrapper.vm.diffFilesLength).toBe(100);
- expect(wrapper.vm.unwatchRetrievingBatches).toHaveBeenCalled();
});
it('calls batch methods if diffsBatchLoad is enabled, and latest version', async () => {
@@ -150,9 +161,7 @@ describe('diffs/components/app', () => {
expect(wrapper.vm.fetchDiffFilesMeta).toHaveBeenCalled();
expect(wrapper.vm.fetchDiffFilesBatch).toHaveBeenCalled();
expect(wrapper.vm.fetchCoverageFiles).toHaveBeenCalled();
- expect(wrapper.vm.unwatchDiscussions).toHaveBeenCalled();
expect(wrapper.vm.diffFilesLength).toBe(100);
- expect(wrapper.vm.unwatchRetrievingBatches).toHaveBeenCalled();
});
});
@@ -187,16 +196,14 @@ describe('diffs/components/app', () => {
wrapper.vm.fetchData(false);
expect(wrapper.vm.fetchCodequality).not.toHaveBeenCalled();
+ expect(codeQualityAndSastQueryHandlerSuccess).not.toHaveBeenCalled();
});
});
describe('SAST diff', () => {
it('does not fetch Sast data on FOSS', () => {
createComponent();
- jest.spyOn(wrapper.vm, 'fetchSast');
- wrapper.vm.fetchData(false);
-
- expect(wrapper.vm.fetchSast).not.toHaveBeenCalled();
+ expect(codeQualityAndSastQueryHandlerSuccess).not.toHaveBeenCalled();
});
});
@@ -652,6 +659,12 @@ describe('diffs/components/app', () => {
});
describe('file-by-file', () => {
+ let hashSpy;
+
+ beforeEach(() => {
+ hashSpy = jest.spyOn(commonUtils, 'handleLocationHash');
+ });
+
it('renders a single diff', async () => {
createComponent(
undefined,
@@ -671,6 +684,48 @@ describe('diffs/components/app', () => {
expect(wrapper.findAllComponents(DiffFile).length).toBe(1);
});
+ describe('rechecking the url hash for scrolling', () => {
+ const advanceAndCheckCalls = (count = 0) => {
+ jest.advanceTimersByTime(DEFAULT_DEBOUNCE_AND_THROTTLE_MS);
+ expect(hashSpy).toHaveBeenCalledTimes(count);
+ };
+
+ it('re-checks one time after the file finishes loading', () => {
+ createComponent(
+ undefined,
+ ({ state }) => {
+ state.diffs.diffFiles = [{ isLoadingFullFile: true }];
+ },
+ undefined,
+ { viewDiffsFileByFile: true },
+ );
+
+ // The hash check is not called if the file is still marked as loading
+ expect(hashSpy).toHaveBeenCalledTimes(0);
+ eventHub.$emit(EVT_DISCUSSIONS_ASSIGNED);
+ advanceAndCheckCalls();
+ eventHub.$emit(EVT_DISCUSSIONS_ASSIGNED);
+ advanceAndCheckCalls();
+ // Once the file has finished loading, it calls through to check the hash
+ store.state.diffs.diffFiles[0].isLoadingFullFile = false;
+ eventHub.$emit(EVT_DISCUSSIONS_ASSIGNED);
+ advanceAndCheckCalls(1);
+ // No further scrolls happen after one hash check / scroll
+ eventHub.$emit(EVT_DISCUSSIONS_ASSIGNED);
+ advanceAndCheckCalls(1);
+ eventHub.$emit(EVT_DISCUSSIONS_ASSIGNED);
+ advanceAndCheckCalls(1);
+ });
+
+ it('does not re-check when not in single-file mode', () => {
+ createComponent();
+
+ eventHub.$emit(EVT_DISCUSSIONS_ASSIGNED);
+
+ expect(hashSpy).not.toHaveBeenCalled();
+ });
+ });
+
describe('pagination', () => {
const fileByFileNav = () => wrapper.find('[data-testid="file-by-file-navigation"]');
const paginator = () => fileByFileNav().findComponent(GlPagination);
@@ -769,6 +824,15 @@ describe('diffs/components/app', () => {
beforeEach(() => {
createComponent();
+
+ store.state.diffs.diffFiles = [
+ {
+ file_hash: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a',
+ highlighted_diff_lines: [],
+ viewer: { manuallyCollapsed: true },
+ },
+ ];
+
loadSpy = jest.spyOn(wrapper.vm, 'loadCollapsedDiff').mockResolvedValue('resolved');
});
@@ -787,5 +851,14 @@ describe('diffs/components/app', () => {
expect(loadSpy).toHaveBeenCalledWith({ file: store.state.diffs.diffFiles[0] });
});
+
+ it('does nothing when file is not collapsed', () => {
+ store.state.diffs.diffFiles[0].viewer.manuallyCollapsed = false;
+ window.location.hash = '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_0_1';
+
+ eventHub.$emit('doneLoadingBatches');
+
+ expect(loadSpy).not.toHaveBeenCalledWith({ file: store.state.diffs.diffFiles[0] });
+ });
});
});
diff --git a/spec/frontend/diffs/components/diff_file_header_spec.js b/spec/frontend/diffs/components/diff_file_header_spec.js
index b0d98e0e4a6..d6539a5bffa 100644
--- a/spec/frontend/diffs/components/diff_file_header_spec.js
+++ b/spec/frontend/diffs/components/diff_file_header_spec.js
@@ -103,7 +103,7 @@ describe('DiffFileHeader component', () => {
const createComponent = ({ props, options = {} } = {}) => {
mockStoreConfig = cloneDeep(defaultMockStoreConfig);
- const store = new Vuex.Store({ ...mockStoreConfig, ...(options.store || {}) });
+ const store = new Vuex.Store({ ...mockStoreConfig, ...options.store });
wrapper = shallowMount(DiffFileHeader, {
propsData: {
diff --git a/spec/frontend/diffs/components/diff_file_spec.js b/spec/frontend/diffs/components/diff_file_spec.js
index 13efd3584b4..34af3d72b04 100644
--- a/spec/frontend/diffs/components/diff_file_spec.js
+++ b/spec/frontend/diffs/components/diff_file_spec.js
@@ -13,6 +13,7 @@ import DiffFileComponent from '~/diffs/components/diff_file.vue';
import DiffFileHeaderComponent from '~/diffs/components/diff_file_header.vue';
import {
+ EVT_DISCUSSIONS_ASSIGNED,
EVT_EXPAND_ALL_FILES,
EVT_PERF_MARK_DIFF_FILES_END,
EVT_PERF_MARK_FIRST_DIFF_FILE_SHOWN,
@@ -271,9 +272,10 @@ describe('DiffFile', () => {
await nextTick(); // Wait for the idleCallback
await nextTick(); // Wait for nextTick inside postRender
- expect(eventHub.$emit).toHaveBeenCalledTimes(2);
+ expect(eventHub.$emit).toHaveBeenCalledTimes(3);
expect(eventHub.$emit).toHaveBeenCalledWith(EVT_PERF_MARK_FIRST_DIFF_FILE_SHOWN);
expect(eventHub.$emit).toHaveBeenCalledWith(EVT_PERF_MARK_DIFF_FILES_END);
+ expect(eventHub.$emit).toHaveBeenCalledWith(EVT_DISCUSSIONS_ASSIGNED);
});
});
});
diff --git a/spec/frontend/diffs/components/shared/__snapshots__/findings_drawer_spec.js.snap b/spec/frontend/diffs/components/shared/__snapshots__/findings_drawer_spec.js.snap
index afa2a7d9678..cfc34bd2f25 100644
--- a/spec/frontend/diffs/components/shared/__snapshots__/findings_drawer_spec.js.snap
+++ b/spec/frontend/diffs/components/shared/__snapshots__/findings_drawer_spec.js.snap
@@ -1,111 +1,220 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`FindingsDrawer matches the snapshot 1`] = `
-<gl-drawer-stub
+<transition-stub
class="findings-drawer"
- headerheight=""
- open="true"
- variant="default"
- zindex="252"
+ name="gl-drawer"
>
- <h2
- class="gl-font-size-h2 gl-mb-0 gl-mt-0"
- data-testid="findings-drawer-heading"
+ <aside
+ class="gl-drawer gl-drawer-default"
+ style="top: 0px; z-index: 252;"
>
- Unused method argument - \`c\`. If it's necessary, use \`_\` or \`_c\` as an argument name to indicate that it won't be used.
- </h2>
- <ul
- class="gl-border-b-initial gl-list-style-none gl-mb-0 gl-pb-0!"
- >
- <li
- class="gl-mb-4"
- data-testid="findings-drawer-severity"
- >
- <span
- class="gl-font-weight-bold"
- >
- Severity:
- </span>
- <gl-icon-stub
- class="gl-text-orange-300 inline-findings-severity-icon"
- data-testid="findings-drawer-severity-icon"
- name="severity-low"
- size="12"
- />
- minor
- </li>
- <li
- class="gl-mb-4"
- data-testid="findings-drawer-engine"
- >
- <span
- class="gl-font-weight-bold"
- >
- Engine:
- </span>
- testengine name
- </li>
- <li
- class="gl-mb-4"
- data-testid="findings-drawer-category"
+ <div
+ class="gl-drawer-header"
>
- <span
- class="gl-font-weight-bold"
+ <div
+ class="gl-drawer-title"
>
- Category:
- </span>
- testcategory 1
- </li>
- <li
- class="gl-mb-4"
- data-testid="findings-drawer-other-locations"
+ <h2
+ class="drawer-heading gl-font-base gl-mb-0 gl-mt-0"
+ >
+ <svg
+ aria-hidden="true"
+ class="gl-icon gl-text-orange-300 gl-vertical-align-baseline! inline-findings-severity-icon s12"
+ data-testid="severity-low-icon"
+ role="img"
+ >
+ <use
+ href="file-mock#severity-low"
+ />
+ </svg>
+ <span
+ class="drawer-heading-severity"
+ >
+ low
+ </span>
+ SAST Finding
+ </h2>
+ <button
+ aria-label="Close drawer"
+ class="btn btn-default btn-default-tertiary btn-icon btn-sm gl-button gl-drawer-close-button"
+ type="button"
+ >
+ <svg
+ aria-hidden="true"
+ class="gl-button-icon gl-icon s16"
+ data-testid="close-icon"
+ role="img"
+ >
+ <use
+ href="file-mock#close"
+ />
+ </svg>
+ </button>
+ </div>
+ </div>
+ <div
+ class="gl-drawer-body gl-drawer-body-scrim"
>
- <span
- class="gl-display-block gl-font-weight-bold gl-mb-3"
- >
- Other locations:
- </span>
<ul
- class="gl-pl-6"
+ class="gl-border-b-initial gl-list-style-none gl-mb-0 gl-pb-0!"
>
<li
- class="gl-mb-1"
+ class="gl-mb-4"
+ >
+ <p
+ class="gl-line-height-20"
+ >
+ <span
+ class="gl-display-block gl-font-weight-bold gl-mb-1"
+ data-testid="findings-drawer-item-description"
+ >
+ Name
+ </span>
+ <span
+ data-testid="findings-drawer-item-value-prop"
+ >
+ mockedtitle
+ </span>
+ </p>
+ </li>
+ <li
+ class="gl-mb-4"
+ >
+ <p
+ class="gl-line-height-20"
+ >
+ <span
+ class="gl-display-block gl-font-weight-bold gl-mb-1"
+ data-testid="findings-drawer-item-description"
+ >
+ Status
+ </span>
+ <span
+ class="badge badge-pill badge-warning gl-badge md text-capitalize"
+ >
+ detected
+ </span>
+ </p>
+ </li>
+ <li
+ class="gl-mb-4"
+ >
+ <p
+ class="gl-line-height-20"
+ >
+ <span
+ class="gl-display-block gl-font-weight-bold gl-mb-1"
+ data-testid="findings-drawer-item-description"
+ >
+ Description
+ </span>
+ <span
+ data-testid="findings-drawer-item-value-prop"
+ >
+ fakedesc
+ </span>
+ </p>
+ </li>
+ <li
+ class="gl-mb-4"
+ >
+ <p
+ class="gl-line-height-20"
+ >
+ <span
+ class="gl-display-block gl-font-weight-bold gl-mb-1"
+ data-testid="findings-drawer-item-description"
+ >
+ Project
+ </span>
+ <a
+ class="gl-link"
+ href="/testpath"
+ >
+ testname
+ </a>
+ </p>
+ </li>
+ <li
+ class="gl-mb-4"
+ >
+ <p
+ class="gl-line-height-20"
+ >
+ <span
+ class="gl-display-block gl-font-weight-bold gl-mb-1"
+ data-testid="findings-drawer-item-description"
+ >
+ File
+ </span>
+ <span
+ data-testid="findings-drawer-item-value-prop"
+ />
+ </p>
+ </li>
+ <li
+ class="gl-mb-4"
>
- <gl-link-stub
- data-testid="findings-drawer-other-locations-link"
- href="http://testlink.com"
+ <p
+ class="gl-line-height-20"
>
- testpath
- </gl-link-stub>
+ <span
+ class="gl-display-block gl-font-weight-bold gl-mb-1"
+ data-testid="findings-drawer-item-description"
+ >
+ Identifiers
+ </span>
+ <span>
+ <a
+ class="gl-link"
+ href="https://semgrep.dev/r/gitlab.eslint.detect-disable-mustache-escape"
+ >
+ eslint.detect-disable-mustache-escape
+ </a>
+ </span>
+ </p>
</li>
<li
- class="gl-mb-1"
+ class="gl-mb-4"
>
- <gl-link-stub
- data-testid="findings-drawer-other-locations-link"
- href="http://testlink.com"
+ <p
+ class="gl-line-height-20"
>
- testpath 1
- </gl-link-stub>
+ <span
+ class="gl-display-block gl-font-weight-bold gl-mb-1"
+ data-testid="findings-drawer-item-description"
+ >
+ Tool
+ </span>
+ <span
+ data-testid="findings-drawer-item-value-prop"
+ >
+ SAST
+ </span>
+ </p>
</li>
<li
- class="gl-mb-1"
+ class="gl-mb-4"
>
- <gl-link-stub
- data-testid="findings-drawer-other-locations-link"
- href="http://testlink.com"
+ <p
+ class="gl-line-height-20"
>
- testpath2
- </gl-link-stub>
+ <span
+ class="gl-display-block gl-font-weight-bold gl-mb-1"
+ data-testid="findings-drawer-item-description"
+ >
+ Engine
+ </span>
+ <span
+ data-testid="findings-drawer-item-value-prop"
+ >
+ testengine name
+ </span>
+ </p>
</li>
</ul>
- </li>
- </ul>
- <span
- class="drawer-body gl-display-block gl-px-3 gl-py-0!"
- data-testid="findings-drawer-body"
- >
- Duplicated Code Duplicated code
- </span>
-</gl-drawer-stub>
+ </div>
+ </aside>
+</transition-stub>
`;
diff --git a/spec/frontend/diffs/components/shared/findings_drawer_item_spec.js b/spec/frontend/diffs/components/shared/findings_drawer_item_spec.js
new file mode 100644
index 00000000000..80087ea66a2
--- /dev/null
+++ b/spec/frontend/diffs/components/shared/findings_drawer_item_spec.js
@@ -0,0 +1,54 @@
+import FindingsDrawerItem from '~/diffs/components/shared/findings_drawer_item.vue';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+
+let wrapper;
+
+const mockDescription = 'testDescription';
+const slotTestId = 'findings-drawer-item-value-slot';
+const mockValue = 'testValue';
+const mockSlot = `<span data-testid="${slotTestId}">mockSlot</span>`;
+const mockSlotText = 'mockSlot';
+
+describe('FindingsDrawerItem', () => {
+ const description = () => wrapper.findByTestId('findings-drawer-item-description');
+
+ const valueSlot = () => wrapper.findByTestId(slotTestId);
+ const valueProp = () => wrapper.findByTestId('findings-drawer-item-value-prop');
+
+ const createWrapper = (props = {}, slots = {}) => {
+ return shallowMountExtended(FindingsDrawerItem, {
+ propsData: {
+ ...props,
+ },
+ slots: {
+ ...slots,
+ },
+ });
+ };
+
+ it('renders with default values', () => {
+ wrapper = createWrapper();
+ expect(description().text()).toContain('');
+ expect(valueProp().text()).toContain('');
+ });
+
+ it('renders description and value props correctly', () => {
+ wrapper = createWrapper({ description: mockDescription, value: mockValue });
+ expect(description().text()).toContain(mockDescription);
+ expect(valueProp().text()).toContain(mockValue);
+ });
+
+ describe('when slot content is passed', () => {
+ it('renders slot content', () => {
+ wrapper = createWrapper({}, { value: mockSlot });
+ expect(valueSlot().text()).toContain(mockSlotText);
+ });
+
+ describe('when value prop is passed', () => {
+ it('does not render value prop', () => {
+ wrapper = createWrapper({ value: mockValue }, { value: mockSlot });
+ expect(valueProp().exists()).toBe(false);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/diffs/components/shared/findings_drawer_spec.js b/spec/frontend/diffs/components/shared/findings_drawer_spec.js
index 0af6e0f0e96..62d875ed9b7 100644
--- a/spec/frontend/diffs/components/shared/findings_drawer_spec.js
+++ b/spec/frontend/diffs/components/shared/findings_drawer_spec.js
@@ -1,16 +1,33 @@
+import { GlDrawer } from '@gitlab/ui';
import FindingsDrawer from '~/diffs/components/shared/findings_drawer.vue';
-import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import mockFinding from '../../mock_data/findings_drawer';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import { mockFinding, mockProject } from '../../mock_data/findings_drawer';
let wrapper;
+const getDrawer = () => wrapper.findComponent(GlDrawer);
+const closeEvent = 'close';
+
+const createWrapper = () => {
+ return mountExtended(FindingsDrawer, {
+ propsData: {
+ drawer: mockFinding,
+ project: mockProject,
+ },
+ });
+};
+
describe('FindingsDrawer', () => {
- const createWrapper = () => {
- return shallowMountExtended(FindingsDrawer, {
- propsData: {
- drawer: mockFinding,
- },
- });
- };
+ it('renders without errors', () => {
+ wrapper = createWrapper();
+ expect(wrapper.exists()).toBe(true);
+ });
+
+ it('emits close event when gl-drawer emits close event', () => {
+ wrapper = createWrapper();
+
+ getDrawer().vm.$emit(closeEvent);
+ expect(wrapper.emitted(closeEvent)).toHaveLength(1);
+ });
it('matches the snapshot', () => {
wrapper = createWrapper();
diff --git a/spec/frontend/diffs/mock_data/findings_drawer.js b/spec/frontend/diffs/mock_data/findings_drawer.js
index d7e7e957c83..4823a18b267 100644
--- a/spec/frontend/diffs/mock_data/findings_drawer.js
+++ b/spec/frontend/diffs/mock_data/findings_drawer.js
@@ -1,21 +1,28 @@
-export default {
+export const mockFinding = {
+ title: 'mockedtitle',
+ state: 'detected',
+ scale: 'sast',
line: 7,
- description:
- "Unused method argument - `c`. If it's necessary, use `_` or `_c` as an argument name to indicate that it won't be used.",
- severity: 'minor',
+ description: 'fakedesc',
+ severity: 'low',
engineName: 'testengine name',
categories: ['testcategory 1', 'testcategory 2'],
content: {
body: 'Duplicated Code Duplicated code',
},
- location: {
- path: 'workhorse/config_test.go',
- lines: { begin: 221, end: 284 },
- },
- otherLocations: [
- { path: 'testpath', href: 'http://testlink.com' },
- { path: 'testpath 1', href: 'http://testlink.com' },
- { path: 'testpath2', href: 'http://testlink.com' },
+ webUrl: {},
+ identifiers: [
+ {
+ __typename: 'VulnerabilityIdentifier',
+ externalId: 'eslint.detect-disable-mustache-escape',
+ externalType: 'semgrep_id',
+ name: 'eslint.detect-disable-mustache-escape',
+ url: 'https://semgrep.dev/r/gitlab.eslint.detect-disable-mustache-escape',
+ },
],
- type: 'issue',
+};
+
+export const mockProject = {
+ nameWithNamespace: 'testname',
+ fullPath: 'testpath',
};
diff --git a/spec/frontend/diffs/store/actions_spec.js b/spec/frontend/diffs/store/actions_spec.js
index 18e81232b5c..8cf376b13e3 100644
--- a/spec/frontend/diffs/store/actions_spec.js
+++ b/spec/frontend/diffs/store/actions_spec.js
@@ -521,7 +521,7 @@ describe('DiffsStoreActions', () => {
return testAction(
diffActions.fetchDiffFilesBatch,
{},
- { endpointBatch, diffViewType: 'inline', diffFiles: [] },
+ { endpointBatch, diffViewType: 'inline', diffFiles: [], perPage: 5 },
[
{ type: types.SET_BATCH_LOADING_STATE, payload: 'loading' },
{ type: types.SET_RETRIEVING_BATCHES, payload: true },
@@ -841,7 +841,7 @@ describe('DiffsStoreActions', () => {
};
const singleDiscussion = {
id: '1',
- file_hash: 'ABC',
+ diff_file: { file_hash: 'ABC' },
line_code: 'ABC_1_1',
};
diff --git a/spec/frontend/diffs/store/utils_spec.js b/spec/frontend/diffs/store/utils_spec.js
index 720b72f4965..6331269d6e8 100644
--- a/spec/frontend/diffs/store/utils_spec.js
+++ b/spec/frontend/diffs/store/utils_spec.js
@@ -330,7 +330,7 @@ describe('DiffsStoreUtils', () => {
old_line: 5,
new_line: 5,
rich_text: '<p>rich</p>', // Note no leading space
- discussionsExpanded: true,
+ discussionsExpanded: false,
discussions: [],
hasForm: false,
text: undefined,
diff --git a/spec/frontend/diffs/utils/sort_errors_by_file_spec.js b/spec/frontend/diffs/utils/sort_findings_by_file_spec.js
index ca8a8ec3516..8dc4f57d98c 100644
--- a/spec/frontend/diffs/utils/sort_errors_by_file_spec.js
+++ b/spec/frontend/diffs/utils/sort_findings_by_file_spec.js
@@ -6,6 +6,10 @@ describe('sort_findings_by_file utilities', () => {
const mockLine = '00';
const mockFile1 = 'file1.js';
const mockFile2 = 'file2.rb';
+ const webUrl1 = 'http://example.com/file1.js';
+ const webUrl2 = 'http://example.com/file2.rb';
+ const engineName1 = 'engineName1';
+ const engineName2 = 'engineName2';
const emptyResponse = {
files: {},
};
@@ -16,12 +20,16 @@ describe('sort_findings_by_file utilities', () => {
filePath: mockFile1,
line: mockLine,
description: mockDescription,
+ webUrl: webUrl1,
+ engineName: engineName1,
},
{
severity: mockSeverity,
filePath: mockFile2,
line: mockLine,
description: mockDescription,
+ webUrl: webUrl2,
+ engineName: engineName2,
},
];
const sortedFindings = {
@@ -29,15 +37,21 @@ describe('sort_findings_by_file utilities', () => {
[mockFile1]: [
{
line: mockLine,
+ filePath: mockFile1,
description: mockDescription,
severity: mockSeverity,
+ webUrl: webUrl1,
+ engineName: engineName1,
},
],
[mockFile2]: [
{
line: mockLine,
+ filePath: mockFile2,
description: mockDescription,
severity: mockSeverity,
+ webUrl: webUrl2,
+ engineName: engineName2,
},
],
},
diff --git a/spec/frontend/emoji/awards_app/store/actions_spec.js b/spec/frontend/emoji/awards_app/store/actions_spec.js
index 65f2e813f19..23aa7bd5ad7 100644
--- a/spec/frontend/emoji/awards_app/store/actions_spec.js
+++ b/spec/frontend/emoji/awards_app/store/actions_spec.js
@@ -1,11 +1,11 @@
-import * as Sentry from '@sentry/browser';
import MockAdapter from 'axios-mock-adapter';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import testAction from 'helpers/vuex_action_helper';
import * as actions from '~/emoji/awards_app/store/actions';
import axios from '~/lib/utils/axios_utils';
import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
-jest.mock('@sentry/browser');
+jest.mock('~/sentry/sentry_browser_wrapper');
jest.mock('~/vue_shared/plugins/global_toast');
describe('Awards app actions', () => {
diff --git a/spec/frontend/emoji/index_spec.js b/spec/frontend/emoji/index_spec.js
index 1a12bd303f1..7d6a45fbf30 100644
--- a/spec/frontend/emoji/index_spec.js
+++ b/spec/frontend/emoji/index_spec.js
@@ -1,10 +1,11 @@
+import MockAdapter from 'axios-mock-adapter';
import {
emojiFixtureMap,
- mockEmojiData,
initEmojiMock,
validEmoji,
invalidEmoji,
clearEmojiMock,
+ mockEmojiData,
} from 'helpers/emoji';
import { trimText } from 'helpers/text_helper';
import { createMockClient } from 'helpers/mock_apollo_helper';
@@ -14,9 +15,10 @@ import {
getEmojiInfo,
sortEmoji,
initEmojiMap,
- getAllEmoji,
+ getEmojiMap,
emojiFallbackImageSrc,
loadCustomEmojiWithNames,
+ EMOJI_VERSION,
} from '~/emoji';
import isEmojiUnicodeSupported, {
@@ -27,8 +29,11 @@ import isEmojiUnicodeSupported, {
isHorceRacingSkinToneComboEmoji,
isPersonZwjEmoji,
} from '~/emoji/support/is_emoji_unicode_supported';
-import { NEUTRAL_INTENT_MULTIPLIER } from '~/emoji/constants';
+import { CACHE_KEY, CACHE_VERSION_KEY, NEUTRAL_INTENT_MULTIPLIER } from '~/emoji/constants';
import customEmojiQuery from '~/emoji/queries/custom_emoji.query.graphql';
+import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
+import { useLocalStorageSpy } from 'jest/__helpers__/local_storage_helper';
let mockClient;
jest.mock('~/lib/graphql', () => {
@@ -75,6 +80,195 @@ function createMockEmojiClient() {
document.body.dataset.groupFullPath = 'test-group';
}
+describe('retrieval of emojis.json', () => {
+ useLocalStorageSpy();
+
+ let mock;
+ beforeEach(() => {
+ mock = new MockAdapter(axios);
+ mock.onGet(/emojis\.json$/).reply(HTTP_STATUS_OK, mockEmojiData);
+ initEmojiMap.promise = null;
+ });
+
+ afterEach(() => {
+ mock.restore();
+ });
+
+ const assertCorrectLocalStorage = () => {
+ expect(localStorage.length).toBe(1);
+ expect(localStorage.getItem(CACHE_KEY)).toBe(
+ JSON.stringify({ data: mockEmojiData, EMOJI_VERSION }),
+ );
+ };
+
+ const assertEmojiBeingLoadedCorrectly = () => {
+ expect(Object.keys(getEmojiMap())).toEqual(Object.keys(validEmoji));
+ };
+
+ it('should remove the old `CACHE_VERSION_KEY`', async () => {
+ localStorage.setItem(CACHE_VERSION_KEY, EMOJI_VERSION);
+
+ await initEmojiMap();
+
+ expect(localStorage.getItem(CACHE_VERSION_KEY)).toBe(null);
+ });
+
+ describe('when the localStorage is empty', () => {
+ it('should call the API and store results in localStorage', async () => {
+ await initEmojiMap();
+
+ assertEmojiBeingLoadedCorrectly();
+ expect(mock.history.get.length).toBe(1);
+ assertCorrectLocalStorage();
+ });
+ });
+
+ describe('when the localStorage stores the correct version', () => {
+ beforeEach(async () => {
+ localStorage.setItem(CACHE_KEY, JSON.stringify({ data: mockEmojiData, EMOJI_VERSION }));
+ localStorage.setItem.mockClear();
+ await initEmojiMap();
+ });
+
+ it('should not call the API and not mutate the localStorage', () => {
+ assertEmojiBeingLoadedCorrectly();
+ expect(mock.history.get.length).toBe(0);
+ expect(localStorage.setItem).not.toHaveBeenCalled();
+ assertCorrectLocalStorage();
+ });
+ });
+
+ describe('when the localStorage stores an incorrect version', () => {
+ beforeEach(async () => {
+ localStorage.setItem(
+ CACHE_KEY,
+ JSON.stringify({ data: mockEmojiData, EMOJI_VERSION: `${EMOJI_VERSION}-different` }),
+ );
+ localStorage.setItem.mockClear();
+ await initEmojiMap();
+ });
+
+ it('should call the API and store results in localStorage', () => {
+ assertEmojiBeingLoadedCorrectly();
+ expect(mock.history.get.length).toBe(1);
+ assertCorrectLocalStorage();
+ });
+ });
+
+ describe('when the localStorage stores corrupted data', () => {
+ beforeEach(async () => {
+ localStorage.setItem(CACHE_KEY, "[invalid: 'INVALID_JSON");
+ localStorage.setItem.mockClear();
+ await initEmojiMap();
+ });
+
+ it('should call the API and store results in localStorage', () => {
+ assertEmojiBeingLoadedCorrectly();
+ expect(mock.history.get.length).toBe(1);
+ assertCorrectLocalStorage();
+ });
+ });
+
+ describe('when the localStorage stores data in a different format', () => {
+ beforeEach(async () => {
+ localStorage.setItem(CACHE_KEY, JSON.stringify([]));
+ localStorage.setItem.mockClear();
+ await initEmojiMap();
+ });
+
+ it('should call the API and store results in localStorage', () => {
+ assertEmojiBeingLoadedCorrectly();
+ expect(mock.history.get.length).toBe(1);
+ assertCorrectLocalStorage();
+ });
+ });
+
+ describe('when the localStorage is full', () => {
+ beforeEach(async () => {
+ const oldSetItem = localStorage.setItem;
+ localStorage.setItem = jest.fn().mockImplementationOnce((key, value) => {
+ if (key === CACHE_KEY) {
+ throw new Error('Storage Full');
+ }
+ oldSetItem(key, value);
+ });
+ await initEmojiMap();
+ });
+
+ it('should call API but not store the results', () => {
+ assertEmojiBeingLoadedCorrectly();
+ expect(mock.history.get.length).toBe(1);
+ expect(localStorage.length).toBe(0);
+ expect(localStorage.setItem).toHaveBeenCalledTimes(1);
+ expect(localStorage.setItem).toHaveBeenCalledWith(
+ CACHE_KEY,
+ JSON.stringify({ data: mockEmojiData, EMOJI_VERSION }),
+ );
+ });
+ });
+
+ describe('backwards compatibility', () => {
+ // As per: https://gitlab.com/gitlab-org/gitlab/-/blob/62b66abd3bb7801e7c85b4e42a1bbd51fbb37c1b/app/assets/javascripts/emoji/index.js#L27-52
+ async function prevImplementation() {
+ if (
+ window.localStorage.getItem(CACHE_VERSION_KEY) === EMOJI_VERSION &&
+ window.localStorage.getItem(CACHE_KEY)
+ ) {
+ return JSON.parse(window.localStorage.getItem(CACHE_KEY));
+ }
+
+ // We load the JSON file direct from the server
+ // because it can't be loaded from a CDN due to
+ // cross domain problems with JSON
+ const { data } = await axios.get(
+ `${gon.relative_url_root || ''}/-/emojis/${EMOJI_VERSION}/emojis.json`,
+ );
+
+ try {
+ window.localStorage.setItem(CACHE_VERSION_KEY, EMOJI_VERSION);
+ window.localStorage.setItem(CACHE_KEY, JSON.stringify(data));
+ } catch {
+ // Setting data in localstorage may fail when storage quota is exceeded.
+ // We should continue even when this fails.
+ }
+
+ return data;
+ }
+
+ it('Old -> New -> Old should not break', async () => {
+ // The follow steps simulate a multi-version deployment. e.g.
+ // Hitting a page on "regular" .com, then canary, and then "regular" again
+
+ // Load emoji the old way to pre-populate the cache
+ let res = await prevImplementation();
+ expect(res).toEqual(mockEmojiData);
+ expect(mock.history.get.length).toBe(1);
+ localStorage.setItem.mockClear();
+
+ // Load emoji the new way
+ await initEmojiMap();
+ expect(mock.history.get.length).toBe(2);
+ assertEmojiBeingLoadedCorrectly();
+ assertCorrectLocalStorage();
+ localStorage.setItem.mockClear();
+
+ // Load emoji the old way to pre-populate the cache
+ res = await prevImplementation();
+ expect(res).toEqual(mockEmojiData);
+ expect(mock.history.get.length).toBe(3);
+ expect(localStorage.setItem.mock.calls).toEqual([
+ [CACHE_VERSION_KEY, EMOJI_VERSION],
+ [CACHE_KEY, JSON.stringify(mockEmojiData)],
+ ]);
+
+ // Load emoji the old way should work again (and be taken from the cache)
+ res = await prevImplementation();
+ expect(res).toEqual(mockEmojiData);
+ expect(mock.history.get.length).toBe(3);
+ });
+ });
+});
+
describe('emoji', () => {
beforeEach(async () => {
await initEmojiMock();
@@ -90,7 +284,7 @@ describe('emoji', () => {
it('should contain valid emoji', async () => {
await initEmojiMap();
- const allEmoji = Object.keys(getAllEmoji());
+ const allEmoji = Object.keys(getEmojiMap());
Object.keys(validEmoji).forEach((key) => {
expect(allEmoji.includes(key)).toBe(true);
});
@@ -99,34 +293,11 @@ describe('emoji', () => {
it('should not contain invalid emoji', async () => {
await initEmojiMap();
- const allEmoji = Object.keys(getAllEmoji());
+ const allEmoji = Object.keys(getEmojiMap());
Object.keys(invalidEmoji).forEach((key) => {
expect(allEmoji.includes(key)).toBe(false);
});
});
-
- it('fixes broken pride emoji', async () => {
- clearEmojiMock();
- await initEmojiMock({
- gay_pride_flag: {
- c: 'flags',
- // Without a zero-width joiner
- e: '🏳🌈',
- name: 'gay_pride_flag',
- u: '6.0',
- },
- });
-
- expect(getAllEmoji()).toEqual({
- gay_pride_flag: {
- c: 'flags',
- // With a zero-width joiner
- e: '🏳️‍🌈',
- name: 'gay_pride_flag',
- u: '6.0',
- },
- });
- });
});
describe('glEmojiTag', () => {
@@ -448,11 +619,11 @@ describe('emoji', () => {
describe('getEmojiInfo', () => {
it.each(['atom', 'five', 'black_heart'])("should return a correct emoji for '%s'", (name) => {
- expect(getEmojiInfo(name)).toEqual(mockEmojiData[name]);
+ expect(getEmojiInfo(name)).toEqual(getEmojiMap()[name]);
});
it('should return fallback emoji by default', () => {
- expect(getEmojiInfo('atjs')).toEqual(mockEmojiData.grey_question);
+ expect(getEmojiInfo('atjs')).toEqual(getEmojiMap().grey_question);
});
it('should return null when fallback is false', () => {
@@ -461,7 +632,7 @@ describe('emoji', () => {
describe('when query is undefined', () => {
it('should return fallback emoji by default', () => {
- expect(getEmojiInfo()).toEqual(mockEmojiData.grey_question);
+ expect(getEmojiInfo()).toEqual(getEmojiMap().grey_question);
});
it('should return null when fallback is false', () => {
@@ -489,9 +660,9 @@ describe('emoji', () => {
}
return {
- emoji: mockEmojiData[name],
+ emoji: getEmojiMap()[name],
field: 'd',
- fieldValue: mockEmojiData[name].d,
+ fieldValue: getEmojiMap()[name].d,
score,
};
})
@@ -542,7 +713,7 @@ describe('emoji', () => {
const { field, score, fieldValue, name } = item;
return {
- emoji: mockEmojiData[name],
+ emoji: getEmojiMap()[name],
field,
fieldValue,
score,
@@ -669,9 +840,9 @@ describe('emoji', () => {
const { field, score, name } = item;
return {
- emoji: mockEmojiData[name],
+ emoji: getEmojiMap()[name],
field,
- fieldValue: mockEmojiData[name][field],
+ fieldValue: getEmojiMap()[name][field],
score,
};
});
@@ -737,7 +908,7 @@ describe('emoji', () => {
it.each`
emoji | src
- ${'thumbsup'} | ${'/-/emojis/2/thumbsup.png'}
+ ${'thumbsup'} | ${'/-/emojis/3/thumbsup.png'}
${'parrot'} | ${'parrot.gif'}
`('returns $src for emoji with name $emoji', ({ emoji, src }) => {
expect(emojiFallbackImageSrc(emoji)).toBe(src);
@@ -757,7 +928,7 @@ describe('emoji', () => {
it('returns empty object', async () => {
const result = await loadCustomEmojiWithNames();
- expect(result).toEqual({});
+ expect(result).toEqual({ emojis: {}, names: [] });
});
});
@@ -769,7 +940,7 @@ describe('emoji', () => {
it('returns empty object', async () => {
const result = await loadCustomEmojiWithNames();
- expect(result).toEqual({});
+ expect(result).toEqual({ emojis: {}, names: [] });
});
});
@@ -778,14 +949,17 @@ describe('emoji', () => {
const result = await loadCustomEmojiWithNames();
expect(result).toEqual({
- parrot: {
- c: 'custom',
- d: 'parrot',
- e: undefined,
- name: 'parrot',
- src: 'parrot.gif',
- u: 'custom',
+ emojis: {
+ parrot: {
+ c: 'custom',
+ d: 'parrot',
+ e: undefined,
+ name: 'parrot',
+ src: 'parrot.gif',
+ u: 'custom',
+ },
},
+ names: ['parrot'],
});
});
});
diff --git a/spec/frontend/environments/environment_form_spec.js b/spec/frontend/environments/environment_form_spec.js
index 5888b22aece..478ac8d6e0e 100644
--- a/spec/frontend/environments/environment_form_spec.js
+++ b/spec/frontend/environments/environment_form_spec.js
@@ -31,6 +31,7 @@ const configuration = {
headers: {
'GitLab-Agent-Id': 2,
'Content-Type': 'application/json',
+ Accept: 'application/json',
},
credentials: 'include',
};
diff --git a/spec/frontend/environments/environments_app_spec.js b/spec/frontend/environments/environments_app_spec.js
index dc450eb2aa7..5ac949e77b6 100644
--- a/spec/frontend/environments/environments_app_spec.js
+++ b/spec/frontend/environments/environments_app_spec.js
@@ -71,7 +71,7 @@ describe('~/environments/components/environments_app.vue', () => {
previousPage: 1,
__typename: 'LocalPageInfo',
},
- location = '?scope=available&page=2&search=prod',
+ location = '?scope=active&page=2&search=prod',
}) => {
setWindowLocation(location);
environmentAppMock.mockReturnValue(environmentsApp);
@@ -96,7 +96,7 @@ describe('~/environments/components/environments_app.vue', () => {
paginationMock = jest.fn();
});
- it('should request available environments if the scope is invalid', async () => {
+ it('should request active environments if the scope is invalid', async () => {
await createWrapperWithMocked({
environmentsApp: resolvedEnvironmentsApp,
folder: resolvedFolder,
@@ -105,7 +105,7 @@ describe('~/environments/components/environments_app.vue', () => {
expect(environmentAppMock).toHaveBeenCalledWith(
expect.anything(),
- expect.objectContaining({ scope: 'available', page: 2 }),
+ expect.objectContaining({ scope: 'active', page: 2 }),
expect.anything(),
expect.anything(),
);
@@ -174,18 +174,25 @@ describe('~/environments/components/environments_app.vue', () => {
expect(button.exists()).toBe(true);
});
- it('should not show a button to open the review app modal if review apps are configured', async () => {
- await createWrapperWithMocked({
- environmentsApp: {
- ...resolvedEnvironmentsApp,
- reviewApp: { canSetupReviewApp: false },
- },
- folder: resolvedFolder,
- });
+ it.each`
+ canSetupReviewApp | hasReviewApp
+ ${false} | ${true}
+ ${true} | ${true}
+ `(
+ 'should not show button to open the review app modal',
+ async ({ canSetupReviewApp, hasReviewApp }) => {
+ await createWrapperWithMocked({
+ environmentsApp: {
+ ...resolvedEnvironmentsApp,
+ reviewApp: { canSetupReviewApp, hasReviewApp },
+ },
+ folder: resolvedFolder,
+ });
- const button = wrapper.findByRole('button', { name: s__('Environments|Enable review apps') });
- expect(button.exists()).toBe(false);
- });
+ const button = wrapper.findByRole('button', { name: s__('Environments|Enable review apps') });
+ expect(button.exists()).toBe(false);
+ },
+ );
it('should not show a button to clean up environments if the user has no permissions', async () => {
await createWrapperWithMocked({
@@ -218,16 +225,16 @@ describe('~/environments/components/environments_app.vue', () => {
});
describe('tabs', () => {
- it('should show tabs for available and stopped environmets', async () => {
+ it('should show tabs for active and stopped environmets', async () => {
await createWrapperWithMocked({
environmentsApp: resolvedEnvironmentsApp,
folder: resolvedFolder,
});
- const [available, stopped] = wrapper.findAllByRole('tab').wrappers;
+ const [active, stopped] = wrapper.findAllByRole('tab').wrappers;
- expect(available.text()).toContain(__('Available'));
- expect(available.text()).toContain(resolvedEnvironmentsApp.availableCount.toString());
+ expect(active.text()).toContain(__('Active'));
+ expect(active.text()).toContain(resolvedEnvironmentsApp.activeCount.toString());
expect(stopped.text()).toContain(__('Stopped'));
expect(stopped.text()).toContain(resolvedEnvironmentsApp.stoppedCount.toString());
});
@@ -372,7 +379,7 @@ describe('~/environments/components/environments_app.vue', () => {
next.trigger('click');
await nextTick();
- expect(window.location.search).toBe('?scope=available&page=3&search=prod');
+ expect(window.location.search).toBe('?scope=active&page=3&search=prod');
});
});
@@ -399,7 +406,7 @@ describe('~/environments/components/environments_app.vue', () => {
await waitForDebounce();
- expect(window.location.search).toBe('?scope=available&page=1&search=hello');
+ expect(window.location.search).toBe('?scope=active&page=1&search=hello');
});
it('should query for the entered parameter', async () => {
diff --git a/spec/frontend/environments/graphql/mock_data.js b/spec/frontend/environments/graphql/mock_data.js
index fd97f19a6ab..7d354566761 100644
--- a/spec/frontend/environments/graphql/mock_data.js
+++ b/spec/frontend/environments/graphql/mock_data.js
@@ -262,16 +262,17 @@ export const environmentsApp = {
review_app: {
can_setup_review_app: true,
all_clusters_empty: true,
+ has_review_app: false,
review_snippet:
'{"deploy_review"=>{"stage"=>"deploy", "script"=>["echo \\"Deploy a review app\\""], "environment"=>{"name"=>"review/$CI_COMMIT_REF_NAME", "url"=>"https://$CI_ENVIRONMENT_SLUG.example.com"}, "only"=>["branches"]}}',
},
can_stop_stale_environments: true,
- available_count: 4,
+ active_count: 4,
stopped_count: 0,
};
export const resolvedEnvironmentsApp = {
- availableCount: 4,
+ activeCount: 4,
environments: [
{
name: 'review',
@@ -471,6 +472,7 @@ export const resolvedEnvironmentsApp = {
reviewApp: {
canSetupReviewApp: true,
allClustersEmpty: true,
+ hasReviewApp: false,
reviewSnippet:
'{"deploy_review"=>{"stage"=>"deploy", "script"=>["echo \\"Deploy a review app\\""], "environment"=>{"name"=>"review/$CI_COMMIT_REF_NAME", "url"=>"https://$CI_ENVIRONMENT_SLUG.example.com"}, "only"=>["branches"]}}',
__typename: 'ReviewApp',
@@ -533,7 +535,7 @@ export const folder = {
has_opened_alert: false,
},
],
- available_count: 2,
+ active_count: 2,
stopped_count: 0,
};
@@ -702,7 +704,7 @@ export const resolvedEnvironment = {
};
export const resolvedFolder = {
- availableCount: 2,
+ activeCount: 2,
environments: [
{
id: 42,
diff --git a/spec/frontend/environments/graphql/resolvers/flux_spec.js b/spec/frontend/environments/graphql/resolvers/flux_spec.js
index aa6f9e120f0..526c98b55b3 100644
--- a/spec/frontend/environments/graphql/resolvers/flux_spec.js
+++ b/spec/frontend/environments/graphql/resolvers/flux_spec.js
@@ -1,4 +1,5 @@
import MockAdapter from 'axios-mock-adapter';
+import { WatchApi } from '@gitlab/cluster-client';
import axios from '~/lib/utils/axios_utils';
import { HTTP_STATUS_OK, HTTP_STATUS_UNAUTHORIZED } from '~/lib/utils/http_status';
import { resolvers } from '~/environments/graphql/resolvers';
@@ -14,8 +15,6 @@ describe('~/frontend/environments/graphql/resolvers', () => {
headers: { 'GitLab-Agent-Id': '1' },
},
};
- const namespace = 'default';
- const environmentName = 'my-environment';
beforeEach(() => {
mockResolvers = resolvers();
@@ -27,114 +26,260 @@ describe('~/frontend/environments/graphql/resolvers', () => {
});
describe('fluxKustomizationStatus', () => {
- const endpoint = `${configuration.basePath}/apis/kustomize.toolkit.fluxcd.io/v1beta1/namespaces/${namespace}/kustomizations/${environmentName}`;
+ const client = { writeQuery: jest.fn() };
const fluxResourcePath =
'kustomize.toolkit.fluxcd.io/v1beta1/namespaces/my-namespace/kustomizations/app';
- const endpointWithFluxResourcePath = `${configuration.basePath}/apis/${fluxResourcePath}`;
+ const endpoint = `${configuration.basePath}/apis/${fluxResourcePath}`;
- it('should request Flux Kustomizations for the provided namespace via the Kubernetes API if the fluxResourcePath is not specified', async () => {
- mock
- .onGet(endpoint, { withCredentials: true, headers: configuration.baseOptions.headers })
- .reply(HTTP_STATUS_OK, {
- status: { conditions: fluxKustomizationsMock },
- });
+ describe('when k8sWatchApi feature is disabled', () => {
+ it('should request Flux Kustomization for the provided fluxResourcePath via the Kubernetes API', async () => {
+ mock
+ .onGet(endpoint, {
+ withCredentials: true,
+ headers: configuration.baseOptions.headers,
+ })
+ .reply(HTTP_STATUS_OK, {
+ status: { conditions: fluxKustomizationsMock },
+ });
+
+ const fluxKustomizationStatus = await mockResolvers.Query.fluxKustomizationStatus(
+ null,
+ {
+ configuration,
+ fluxResourcePath,
+ },
+ { client },
+ );
- const fluxKustomizationStatus = await mockResolvers.Query.fluxKustomizationStatus(null, {
- configuration,
- namespace,
- environmentName,
+ expect(fluxKustomizationStatus).toEqual(fluxKustomizationsMock);
});
+ it('should throw an error if the API call fails', async () => {
+ const apiError = 'Invalid credentials';
+ mock
+ .onGet(endpoint, { withCredentials: true, headers: configuration.base })
+ .reply(HTTP_STATUS_UNAUTHORIZED, { message: apiError });
+
+ const fluxKustomizationsError = mockResolvers.Query.fluxKustomizationStatus(
+ null,
+ {
+ configuration,
+ fluxResourcePath,
+ },
+ { client },
+ );
- expect(fluxKustomizationStatus).toEqual(fluxKustomizationsMock);
+ await expect(fluxKustomizationsError).rejects.toThrow(apiError);
+ });
});
- it('should request Flux Kustomization for the provided fluxResourcePath via the Kubernetes API', async () => {
- mock
- .onGet(endpointWithFluxResourcePath, {
- withCredentials: true,
- headers: configuration.baseOptions.headers,
- })
- .reply(HTTP_STATUS_OK, {
- status: { conditions: fluxKustomizationsMock },
- });
- const fluxKustomizationStatus = await mockResolvers.Query.fluxKustomizationStatus(null, {
- configuration,
- namespace,
- environmentName,
- fluxResourcePath,
+ describe('when k8sWatchApi feature is enabled', () => {
+ const mockWatcher = WatchApi.prototype;
+ const mockKustomizationStatusFn = jest.fn().mockImplementation(() => {
+ return Promise.resolve(mockWatcher);
});
+ const mockOnDataFn = jest.fn().mockImplementation((eventName, callback) => {
+ if (eventName === 'data') {
+ callback(fluxKustomizationsMock);
+ }
+ });
+ const resourceName = 'custom-resource';
+ const resourceNamespace = 'custom-namespace';
+ const apiVersion = 'kustomize.toolkit.fluxcd.io/v1beta1';
- expect(fluxKustomizationStatus).toEqual(fluxKustomizationsMock);
- });
- it('should throw an error if the API call fails', async () => {
- const apiError = 'Invalid credentials';
- mock
- .onGet(endpoint, { withCredentials: true, headers: configuration.base })
- .reply(HTTP_STATUS_UNAUTHORIZED, { message: apiError });
-
- const fluxKustomizationsError = mockResolvers.Query.fluxKustomizationStatus(null, {
- configuration,
- namespace,
- environmentName,
+ beforeEach(() => {
+ gon.features = { k8sWatchApi: true };
+ jest.spyOn(mockWatcher, 'subscribeToStream').mockImplementation(mockKustomizationStatusFn);
+ jest.spyOn(mockWatcher, 'on').mockImplementation(mockOnDataFn);
+ });
+
+ describe('when the Kustomization data is present', () => {
+ beforeEach(() => {
+ mock
+ .onGet(endpoint, { withCredentials: true, headers: configuration.baseOptions.headers })
+ .reply(HTTP_STATUS_OK, {
+ apiVersion,
+ metadata: { name: resourceName, namespace: resourceNamespace },
+ status: { conditions: fluxKustomizationsMock },
+ });
+ });
+ it('should watch Kustomization by the metadata name from the cluster_client library when the data is present', async () => {
+ await mockResolvers.Query.fluxKustomizationStatus(
+ null,
+ {
+ configuration,
+ fluxResourcePath,
+ },
+ { client },
+ );
+
+ expect(mockKustomizationStatusFn).toHaveBeenCalledWith(
+ `/apis/${apiVersion}/namespaces/${resourceNamespace}/kustomizations`,
+ {
+ watch: true,
+ fieldSelector: `metadata.name=${decodeURIComponent(resourceName)}`,
+ },
+ );
+ });
+
+ it('should return data when received from the library', async () => {
+ const kustomizationStatus = await mockResolvers.Query.fluxKustomizationStatus(
+ null,
+ {
+ configuration,
+ fluxResourcePath,
+ },
+ { client },
+ );
+
+ expect(kustomizationStatus).toEqual(fluxKustomizationsMock);
+ });
});
- await expect(fluxKustomizationsError).rejects.toThrow(apiError);
+ it('should not watch Kustomization by the metadata name from the cluster_client library when the data is not present', async () => {
+ mock
+ .onGet(endpoint, { withCredentials: true, headers: configuration.baseOptions.headers })
+ .reply(HTTP_STATUS_OK, {});
+
+ await mockResolvers.Query.fluxKustomizationStatus(
+ null,
+ {
+ configuration,
+ fluxResourcePath,
+ },
+ { client },
+ );
+
+ expect(mockKustomizationStatusFn).not.toHaveBeenCalled();
+ });
});
});
describe('fluxHelmReleaseStatus', () => {
- const endpoint = `${configuration.basePath}/apis/helm.toolkit.fluxcd.io/v2beta1/namespaces/${namespace}/helmreleases/${environmentName}`;
+ const client = { writeQuery: jest.fn() };
const fluxResourcePath =
'helm.toolkit.fluxcd.io/v2beta1/namespaces/my-namespace/helmreleases/app';
- const endpointWithFluxResourcePath = `${configuration.basePath}/apis/${fluxResourcePath}`;
+ const endpoint = `${configuration.basePath}/apis/${fluxResourcePath}`;
- it('should request Flux Helm Releases via the Kubernetes API', async () => {
- mock
- .onGet(endpoint, { withCredentials: true, headers: configuration.baseOptions.headers })
- .reply(HTTP_STATUS_OK, {
- status: { conditions: fluxKustomizationsMock },
- });
+ describe('when k8sWatchApi feature is disabled', () => {
+ it('should request Flux HelmRelease for the provided fluxResourcePath via the Kubernetes API', async () => {
+ mock
+ .onGet(endpoint, {
+ withCredentials: true,
+ headers: configuration.baseOptions.headers,
+ })
+ .reply(HTTP_STATUS_OK, {
+ status: { conditions: fluxKustomizationsMock },
+ });
+
+ const fluxHelmReleaseStatus = await mockResolvers.Query.fluxHelmReleaseStatus(
+ null,
+ {
+ configuration,
+ fluxResourcePath,
+ },
+ { client },
+ );
- const fluxHelmReleaseStatus = await mockResolvers.Query.fluxHelmReleaseStatus(null, {
- configuration,
- namespace,
- environmentName,
+ expect(fluxHelmReleaseStatus).toEqual(fluxKustomizationsMock);
});
+ it('should throw an error if the API call fails', async () => {
+ const apiError = 'Invalid credentials';
+ mock
+ .onGet(endpoint, { withCredentials: true, headers: configuration.base })
+ .reply(HTTP_STATUS_UNAUTHORIZED, { message: apiError });
+
+ const fluxHelmReleasesError = mockResolvers.Query.fluxHelmReleaseStatus(
+ null,
+ {
+ configuration,
+ fluxResourcePath,
+ },
+ { client },
+ );
- expect(fluxHelmReleaseStatus).toEqual(fluxKustomizationsMock);
+ await expect(fluxHelmReleasesError).rejects.toThrow(apiError);
+ });
});
- it('should request Flux HelmRelease for the provided fluxResourcePath via the Kubernetes API', async () => {
- mock
- .onGet(endpointWithFluxResourcePath, {
- withCredentials: true,
- headers: configuration.baseOptions.headers,
- })
- .reply(HTTP_STATUS_OK, {
- status: { conditions: fluxKustomizationsMock },
- });
- const fluxHelmReleaseStatus = await mockResolvers.Query.fluxHelmReleaseStatus(null, {
- configuration,
- namespace,
- environmentName,
- fluxResourcePath,
+ describe('when k8sWatchApi feature is enabled', () => {
+ const mockWatcher = WatchApi.prototype;
+ const mockHelmReleaseStatusFn = jest.fn().mockImplementation(() => {
+ return Promise.resolve(mockWatcher);
});
+ const mockOnDataFn = jest.fn().mockImplementation((eventName, callback) => {
+ if (eventName === 'data') {
+ callback(fluxKustomizationsMock);
+ }
+ });
+ const resourceName = 'custom-resource';
+ const resourceNamespace = 'custom-namespace';
+ const apiVersion = 'helm.toolkit.fluxcd.io/v2beta1';
- expect(fluxHelmReleaseStatus).toEqual(fluxKustomizationsMock);
- });
- it('should throw an error if the API call fails', async () => {
- const apiError = 'Invalid credentials';
- mock
- .onGet(endpoint, { withCredentials: true, headers: configuration.base })
- .reply(HTTP_STATUS_UNAUTHORIZED, { message: apiError });
-
- const fluxHelmReleasesError = mockResolvers.Query.fluxHelmReleaseStatus(null, {
- configuration,
- namespace,
- environmentName,
+ beforeEach(() => {
+ gon.features = { k8sWatchApi: true };
+ jest.spyOn(mockWatcher, 'subscribeToStream').mockImplementation(mockHelmReleaseStatusFn);
+ jest.spyOn(mockWatcher, 'on').mockImplementation(mockOnDataFn);
+ });
+
+ describe('when the HelmRelease data is present', () => {
+ beforeEach(() => {
+ mock
+ .onGet(endpoint, { withCredentials: true, headers: configuration.baseOptions.headers })
+ .reply(HTTP_STATUS_OK, {
+ apiVersion,
+ metadata: { name: resourceName, namespace: resourceNamespace },
+ status: { conditions: fluxKustomizationsMock },
+ });
+ });
+ it('should watch HelmRelease by the metadata name from the cluster_client library when the data is present', async () => {
+ await mockResolvers.Query.fluxHelmReleaseStatus(
+ null,
+ {
+ configuration,
+ fluxResourcePath,
+ },
+ { client },
+ );
+
+ expect(mockHelmReleaseStatusFn).toHaveBeenCalledWith(
+ `/apis/${apiVersion}/namespaces/${resourceNamespace}/helmreleases`,
+ {
+ watch: true,
+ fieldSelector: `metadata.name=${decodeURIComponent(resourceName)}`,
+ },
+ );
+ });
+
+ it('should return data when received from the library', async () => {
+ const fluxHelmReleaseStatus = await mockResolvers.Query.fluxHelmReleaseStatus(
+ null,
+ {
+ configuration,
+ fluxResourcePath,
+ },
+ { client },
+ );
+
+ expect(fluxHelmReleaseStatus).toEqual(fluxKustomizationsMock);
+ });
});
- await expect(fluxHelmReleasesError).rejects.toThrow(apiError);
+ it('should not watch Kustomization by the metadata name from the cluster_client library when the data is not present', async () => {
+ mock
+ .onGet(endpoint, { withCredentials: true, headers: configuration.baseOptions.headers })
+ .reply(HTTP_STATUS_OK, {});
+
+ await mockResolvers.Query.fluxHelmReleaseStatus(
+ null,
+ {
+ configuration,
+ fluxResourcePath,
+ },
+ { client },
+ );
+
+ expect(mockHelmReleaseStatusFn).not.toHaveBeenCalled();
+ });
});
});
});
diff --git a/spec/frontend/environments/graphql/resolvers/kubernetes_spec.js b/spec/frontend/environments/graphql/resolvers/kubernetes_spec.js
index ed15c66f4c6..f244ddb01b5 100644
--- a/spec/frontend/environments/graphql/resolvers/kubernetes_spec.js
+++ b/spec/frontend/environments/graphql/resolvers/kubernetes_spec.js
@@ -1,8 +1,9 @@
import MockAdapter from 'axios-mock-adapter';
-import { CoreV1Api, AppsV1Api, BatchV1Api } from '@gitlab/cluster-client';
+import { CoreV1Api, AppsV1Api, BatchV1Api, WatchApi } from '@gitlab/cluster-client';
import axios from '~/lib/utils/axios_utils';
import { resolvers } from '~/environments/graphql/resolvers';
import { CLUSTER_AGENT_ERROR_MESSAGES } from '~/environments/constants';
+import k8sPodsQuery from '~/environments/graphql/queries/k8s_pods.query.graphql';
import { k8sPodsMock, k8sServicesMock, k8sNamespacesMock } from '../mock_data';
describe('~/frontend/environments/graphql/resolvers', () => {
@@ -27,6 +28,7 @@ describe('~/frontend/environments/graphql/resolvers', () => {
});
describe('k8sPods', () => {
+ const client = { writeQuery: jest.fn() };
const mockPodsListFn = jest.fn().mockImplementation(() => {
return Promise.resolve({
items: k8sPodsMock,
@@ -36,39 +38,122 @@ describe('~/frontend/environments/graphql/resolvers', () => {
const mockNamespacedPodsListFn = jest.fn().mockImplementation(mockPodsListFn);
const mockAllPodsListFn = jest.fn().mockImplementation(mockPodsListFn);
- beforeEach(() => {
- jest
- .spyOn(CoreV1Api.prototype, 'listCoreV1NamespacedPod')
- .mockImplementation(mockNamespacedPodsListFn);
- jest
- .spyOn(CoreV1Api.prototype, 'listCoreV1PodForAllNamespaces')
- .mockImplementation(mockAllPodsListFn);
- });
+ describe('when k8sWatchApi feature is disabled', () => {
+ beforeEach(() => {
+ jest
+ .spyOn(CoreV1Api.prototype, 'listCoreV1NamespacedPod')
+ .mockImplementation(mockNamespacedPodsListFn);
+ jest
+ .spyOn(CoreV1Api.prototype, 'listCoreV1PodForAllNamespaces')
+ .mockImplementation(mockAllPodsListFn);
+ });
+
+ it('should request namespaced pods from the cluster_client library if namespace is specified', async () => {
+ const pods = await mockResolvers.Query.k8sPods(
+ null,
+ {
+ configuration,
+ namespace,
+ },
+ { client },
+ );
- it('should request namespaced pods from the cluster_client library if namespace is specified', async () => {
- const pods = await mockResolvers.Query.k8sPods(null, { configuration, namespace });
+ expect(mockNamespacedPodsListFn).toHaveBeenCalledWith({ namespace });
+ expect(mockAllPodsListFn).not.toHaveBeenCalled();
- expect(mockNamespacedPodsListFn).toHaveBeenCalledWith({ namespace });
- expect(mockAllPodsListFn).not.toHaveBeenCalled();
+ expect(pods).toEqual(k8sPodsMock);
+ });
+ it('should request all pods from the cluster_client library if namespace is not specified', async () => {
+ const pods = await mockResolvers.Query.k8sPods(
+ null,
+ {
+ configuration,
+ namespace: '',
+ },
+ { client },
+ );
- expect(pods).toEqual(k8sPodsMock);
- });
- it('should request all pods from the cluster_client library if namespace is not specified', async () => {
- const pods = await mockResolvers.Query.k8sPods(null, { configuration, namespace: '' });
+ expect(mockAllPodsListFn).toHaveBeenCalled();
+ expect(mockNamespacedPodsListFn).not.toHaveBeenCalled();
- expect(mockAllPodsListFn).toHaveBeenCalled();
- expect(mockNamespacedPodsListFn).not.toHaveBeenCalled();
+ expect(pods).toEqual(k8sPodsMock);
+ });
+ it('should throw an error if the API call fails', async () => {
+ jest
+ .spyOn(CoreV1Api.prototype, 'listCoreV1PodForAllNamespaces')
+ .mockRejectedValue(new Error('API error'));
- expect(pods).toEqual(k8sPodsMock);
+ await expect(
+ mockResolvers.Query.k8sPods(null, { configuration }, { client }),
+ ).rejects.toThrow('API error');
+ });
});
- it('should throw an error if the API call fails', async () => {
- jest
- .spyOn(CoreV1Api.prototype, 'listCoreV1PodForAllNamespaces')
- .mockRejectedValue(new Error('API error'));
- await expect(mockResolvers.Query.k8sPods(null, { configuration })).rejects.toThrow(
- 'API error',
- );
+ describe('when k8sWatchApi feature is enabled', () => {
+ const mockWatcher = WatchApi.prototype;
+ const mockPodsListWatcherFn = jest.fn().mockImplementation(() => {
+ return Promise.resolve(mockWatcher);
+ });
+
+ const mockOnDataFn = jest.fn().mockImplementation((eventName, callback) => {
+ if (eventName === 'data') {
+ callback([]);
+ }
+ });
+
+ describe('when the pods data is present', () => {
+ beforeEach(() => {
+ gon.features = { k8sWatchApi: true };
+
+ jest
+ .spyOn(CoreV1Api.prototype, 'listCoreV1NamespacedPod')
+ .mockImplementation(mockNamespacedPodsListFn);
+ jest
+ .spyOn(CoreV1Api.prototype, 'listCoreV1PodForAllNamespaces')
+ .mockImplementation(mockAllPodsListFn);
+ jest.spyOn(mockWatcher, 'subscribeToStream').mockImplementation(mockPodsListWatcherFn);
+ jest.spyOn(mockWatcher, 'on').mockImplementation(mockOnDataFn);
+ });
+
+ it('should request namespaced pods from the cluster_client library if namespace is specified', async () => {
+ await mockResolvers.Query.k8sPods(null, { configuration, namespace }, { client });
+
+ expect(mockPodsListWatcherFn).toHaveBeenCalledWith(
+ `/api/v1/namespaces/${namespace}/pods`,
+ {
+ watch: true,
+ },
+ );
+ });
+ it('should request all pods from the cluster_client library if namespace is not specified', async () => {
+ await mockResolvers.Query.k8sPods(null, { configuration, namespace: '' }, { client });
+
+ expect(mockPodsListWatcherFn).toHaveBeenCalledWith(`/api/v1/pods`, { watch: true });
+ });
+ it('should update cache with the new data when received from the library', async () => {
+ await mockResolvers.Query.k8sPods(null, { configuration, namespace: '' }, { client });
+
+ expect(client.writeQuery).toHaveBeenCalledWith({
+ query: k8sPodsQuery,
+ variables: { configuration, namespace: '' },
+ data: { k8sPods: [] },
+ });
+ });
+ });
+
+ it('should not watch pods from the cluster_client library when the pods data is not present', async () => {
+ jest.spyOn(CoreV1Api.prototype, 'listCoreV1NamespacedPod').mockImplementation(
+ jest.fn().mockImplementation(() => {
+ return Promise.resolve({
+ items: [],
+ });
+ }),
+ );
+
+ await mockResolvers.Query.k8sPods(null, { configuration, namespace }, { client });
+
+ expect(mockPodsListWatcherFn).not.toHaveBeenCalled();
+ });
});
});
describe('k8sServices', () => {
diff --git a/spec/frontend/environments/kubernetes_overview_spec.js b/spec/frontend/environments/kubernetes_overview_spec.js
index 2b810aac653..12689df586f 100644
--- a/spec/frontend/environments/kubernetes_overview_spec.js
+++ b/spec/frontend/environments/kubernetes_overview_spec.js
@@ -30,6 +30,7 @@ const configuration = {
headers: {
'GitLab-Agent-Id': '1',
'Content-Type': 'application/json',
+ Accept: 'application/json',
},
credentials: 'include',
};
@@ -121,7 +122,6 @@ describe('~/environments/components/kubernetes_overview.vue', () => {
expect(findKubernetesStatusBar().props()).toEqual({
clusterHealthStatus: 'success',
configuration,
- namespace: kubernetesNamespace,
environmentName: resolvedEnvironment.name,
fluxResourcePath: fluxResourcePathMock,
});
diff --git a/spec/frontend/environments/kubernetes_status_bar_spec.js b/spec/frontend/environments/kubernetes_status_bar_spec.js
index 5dec7ca5aac..dcd628354e1 100644
--- a/spec/frontend/environments/kubernetes_status_bar_spec.js
+++ b/spec/frontend/environments/kubernetes_status_bar_spec.js
@@ -49,7 +49,6 @@ describe('~/environments/components/kubernetes_status_bar.vue', () => {
const createWrapper = ({
apolloProvider = createApolloProvider(),
clusterHealthStatus = '',
- namespace = '',
fluxResourcePath = '',
} = {}) => {
wrapper = shallowMountExtended(KubernetesStatusBar, {
@@ -57,7 +56,6 @@ describe('~/environments/components/kubernetes_status_bar.vue', () => {
clusterHealthStatus,
configuration,
environmentName,
- namespace,
fluxResourcePath,
},
apolloProvider,
@@ -88,7 +86,7 @@ describe('~/environments/components/kubernetes_status_bar.vue', () => {
});
describe('sync badge', () => {
- describe('when no namespace is provided', () => {
+ describe('when no flux resource path is provided', () => {
beforeEach(() => {
createWrapper();
});
@@ -104,7 +102,6 @@ describe('~/environments/components/kubernetes_status_bar.vue', () => {
});
describe('when flux resource path is provided', () => {
- const namespace = 'my-namespace';
let fluxResourcePath;
describe('if the provided resource is a Kustomization', () => {
@@ -112,7 +109,7 @@ describe('~/environments/components/kubernetes_status_bar.vue', () => {
fluxResourcePath =
'kustomize.toolkit.fluxcd.io/v1beta1/namespaces/my-namespace/kustomizations/app';
- createWrapper({ namespace, fluxResourcePath });
+ createWrapper({ fluxResourcePath });
});
it('requests the Kustomization resource status', () => {
@@ -120,8 +117,6 @@ describe('~/environments/components/kubernetes_status_bar.vue', () => {
{},
expect.objectContaining({
configuration,
- namespace,
- environmentName,
fluxResourcePath,
}),
expect.any(Object),
@@ -139,7 +134,7 @@ describe('~/environments/components/kubernetes_status_bar.vue', () => {
fluxResourcePath =
'helm.toolkit.fluxcd.io/v2beta1/namespaces/my-namespace/helmreleases/app';
- createWrapper({ namespace, fluxResourcePath });
+ createWrapper({ fluxResourcePath });
});
it('requests the HelmRelease resource status', () => {
@@ -147,8 +142,6 @@ describe('~/environments/components/kubernetes_status_bar.vue', () => {
{},
expect.objectContaining({
configuration,
- namespace,
- environmentName,
fluxResourcePath,
}),
expect.any(Object),
@@ -160,30 +153,6 @@ describe('~/environments/components/kubernetes_status_bar.vue', () => {
expect(fluxKustomizationStatusQuery).not.toHaveBeenCalled();
});
});
- });
-
- describe('when namespace is provided', () => {
- describe('with no Flux resources found', () => {
- beforeEach(() => {
- createWrapper({ namespace: 'my-namespace' });
- });
-
- it('requests Kustomizations', () => {
- expect(fluxKustomizationStatusQuery).toHaveBeenCalled();
- });
-
- it('requests HelmReleases when there were no Kustomizations found', async () => {
- await waitForPromises();
-
- expect(fluxHelmReleaseStatusQuery).toHaveBeenCalled();
- });
-
- it('renders sync status as Unavailable when no Kustomizations and HelmReleases found', async () => {
- await waitForPromises();
-
- expect(findSyncBadge().text()).toBe(s__('Deployment|Unavailable'));
- });
- });
describe('with Flux Kustomizations available', () => {
const createApolloProviderWithKustomizations = ({
@@ -202,63 +171,11 @@ describe('~/environments/components/kubernetes_status_bar.vue', () => {
it("doesn't request HelmReleases when the Kustomizations were found", async () => {
createWrapper({
apolloProvider: createApolloProviderWithKustomizations(),
- namespace: 'my-namespace',
});
await waitForPromises();
expect(fluxHelmReleaseStatusQuery).not.toHaveBeenCalled();
});
-
- it.each`
- status | type | badgeType
- ${'True'} | ${'Stalled'} | ${'stalled'}
- ${'True'} | ${'Reconciling'} | ${'reconciling'}
- ${'True'} | ${'Ready'} | ${'reconciled'}
- ${'False'} | ${'Ready'} | ${'failed'}
- ${'True'} | ${'Unknown'} | ${'unknown'}
- `(
- 'renders $badgeType when status is $status and type is $type',
- async ({ status, type, badgeType }) => {
- createWrapper({
- apolloProvider: createApolloProviderWithKustomizations({
- result: { status, type, message: '' },
- }),
- namespace: 'my-namespace',
- });
- await waitForPromises();
-
- const badge = SYNC_STATUS_BADGES[badgeType];
-
- expect(findSyncBadge().text()).toBe(badge.text);
- expect(findSyncBadge().props()).toMatchObject({
- icon: badge.icon,
- variant: badge.variant,
- });
- },
- );
-
- it.each`
- status | type | message | popoverTitle | popoverText
- ${'True'} | ${'Stalled'} | ${'stalled reason'} | ${s__('Deployment|Flux sync stalled')} | ${'stalled reason'}
- ${'True'} | ${'Reconciling'} | ${''} | ${undefined} | ${s__('Deployment|Flux sync reconciling')}
- ${'True'} | ${'Ready'} | ${''} | ${undefined} | ${s__('Deployment|Flux sync reconciled successfully')}
- ${'False'} | ${'Ready'} | ${'failed reason'} | ${s__('Deployment|Flux sync failed')} | ${'failed reason'}
- ${'True'} | ${'Unknown'} | ${''} | ${s__('Deployment|Flux sync status is unknown')} | ${s__('Deployment|Unable to detect state. %{linkStart}How are states detected?%{linkEnd}')}
- `(
- 'renders correct popover text when status is $status and type is $type',
- async ({ status, type, message, popoverTitle, popoverText }) => {
- createWrapper({
- apolloProvider: createApolloProviderWithKustomizations({
- result: { status, type, message },
- }),
- namespace: 'my-namespace',
- });
- await waitForPromises();
-
- expect(findPopover().text()).toMatchInterpolatedText(popoverText);
- expect(findPopover().props('title')).toBe(popoverTitle);
- },
- );
});
describe('when Flux API errored', () => {
@@ -277,7 +194,8 @@ describe('~/environments/components/kubernetes_status_bar.vue', () => {
beforeEach(async () => {
createWrapper({
apolloProvider: createApolloProviderWithErrors(),
- namespace: 'my-namespace',
+ fluxResourcePath:
+ 'kustomize.toolkit.fluxcd.io/v1beta1/namespaces/my-namespace/kustomizations/app',
});
await waitForPromises();
});
diff --git a/spec/frontend/environments/kubernetes_summary_spec.js b/spec/frontend/environments/kubernetes_summary_spec.js
index fdcf32e7d01..457d1a37c1d 100644
--- a/spec/frontend/environments/kubernetes_summary_spec.js
+++ b/spec/frontend/environments/kubernetes_summary_spec.js
@@ -16,7 +16,11 @@ describe('~/environments/components/kubernetes_summary.vue', () => {
const namespace = 'my-kubernetes-namespace';
const configuration = {
basePath: mockKasTunnelUrl,
- headers: { 'GitLab-Agent-Id': '1', 'Content-Type': 'application/json' },
+ headers: {
+ 'GitLab-Agent-Id': '1',
+ 'Content-Type': 'application/json',
+ Accept: 'application/json',
+ },
};
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
diff --git a/spec/frontend/error_tracking/components/error_details_spec.js b/spec/frontend/error_tracking/components/error_details_spec.js
index d6cf12587b9..977e0a55a99 100644
--- a/spec/frontend/error_tracking/components/error_details_spec.js
+++ b/spec/frontend/error_tracking/components/error_details_spec.js
@@ -190,7 +190,7 @@ describe('ErrorDetails', () => {
});
describe('unsafe chars for culprit field', () => {
- const findReportedText = () => wrapper.find('[data-qa-selector="reported_text"]');
+ const findReportedText = () => wrapper.find('[data-testid="reported-text"]');
const culprit = '<script>console.log("surprise!")</script>';
beforeEach(() => {
store.state.details.loadingStacktrace = false;
@@ -350,7 +350,7 @@ describe('ErrorDetails', () => {
it('should submit the form', () => {
window.HTMLFormElement.prototype.submit = () => {};
const submitSpy = jest.spyOn(wrapper.vm.$refs.sentryIssueForm, 'submit');
- wrapper.find('[data-qa-selector="create_issue_button"]').vm.$emit('click');
+ wrapper.find('[data-testid="create-issue-button"]').vm.$emit('click');
expect(submitSpy).toHaveBeenCalled();
submitSpy.mockRestore();
});
@@ -462,7 +462,7 @@ describe('ErrorDetails', () => {
describe('GitLab issue link', () => {
const gitlabIssuePath = 'https://gitlab.example.com/issues/1';
const findGitLabLink = () => wrapper.find(`[href="${gitlabIssuePath}"]`);
- const findCreateIssueButton = () => wrapper.find('[data-qa-selector="create_issue_button"]');
+ const findCreateIssueButton = () => wrapper.find('[data-testid="create-issue-button"]');
const findViewIssueButton = () => wrapper.find('[data-qa-selector="view_issue_button"]');
describe('is present', () => {
@@ -562,7 +562,7 @@ describe('ErrorDetails', () => {
});
it('should track create issue button click', async () => {
- await wrapper.find('[data-qa-selector="create_issue_button"]').vm.$emit('click');
+ await wrapper.find('[data-testid="create-issue-button"]').vm.$emit('click');
expect(Tracking.event).toHaveBeenCalledWith(category, 'click_create_issue_from_error', {
extra: {
variant: integrated ? 'integrated' : 'external',
diff --git a/spec/frontend/fixtures/issues.rb b/spec/frontend/fixtures/issues.rb
index 90aa0544526..6d81d9ca1d2 100644
--- a/spec/frontend/fixtures/issues.rb
+++ b/spec/frontend/fixtures/issues.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe Projects::IssuesController, '(JavaScript fixtures)', :with_license, type: :controller do
include JavaScriptFixturesHelpers
- let(:user) { create(:user, :no_super_sidebar, feed_token: 'feedtoken:coldfeed') }
+ let(:user) { create(:user, feed_token: 'feedtoken:coldfeed') }
let(:namespace) { create(:namespace, name: 'frontend-fixtures') }
let(:project) { create(:project_empty_repo, namespace: namespace, path: 'issues-project') }
diff --git a/spec/frontend/fixtures/merge_requests.rb b/spec/frontend/fixtures/merge_requests.rb
index a1896a6470b..e8272a1f93a 100644
--- a/spec/frontend/fixtures/merge_requests.rb
+++ b/spec/frontend/fixtures/merge_requests.rb
@@ -2,7 +2,13 @@
require 'spec_helper'
-RSpec.describe Projects::MergeRequestsController, '(JavaScript fixtures)', type: :controller do
+RSpec
+ .describe(
+ Projects::MergeRequestsController,
+ '(JavaScript fixtures)',
+ type: :controller,
+ feature_category: :code_review_workflow
+ ) do
include JavaScriptFixturesHelpers
let(:namespace) { create(:namespace, name: 'frontend-fixtures') }
diff --git a/spec/frontend/fixtures/snippet.rb b/spec/frontend/fixtures/snippet.rb
index 23df89a244c..a96b7a57106 100644
--- a/spec/frontend/fixtures/snippet.rb
+++ b/spec/frontend/fixtures/snippet.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe SnippetsController, '(JavaScript fixtures)', type: :controller do
include JavaScriptFixturesHelpers
- let(:user) { create(:user, :no_super_sidebar) }
+ let(:user) { create(:user) }
let(:namespace) { create(:namespace, name: 'frontend-fixtures', owner: user) }
let(:project) { create(:project, :repository, namespace: namespace, path: 'branches-project') }
let(:snippet) { create(:personal_snippet, :public, title: 'snippet.md', content: '# snippet', file_name: 'snippet.md', author: user) }
diff --git a/spec/frontend/fixtures/startup_css.rb b/spec/frontend/fixtures/startup_css.rb
deleted file mode 100644
index 83e02470321..00000000000
--- a/spec/frontend/fixtures/startup_css.rb
+++ /dev/null
@@ -1,89 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe 'Startup CSS fixtures', type: :controller do
- include JavaScriptFixturesHelpers
-
- let(:use_full_html) { true }
-
- render_views
-
- shared_examples 'startup css project fixtures' do |type|
- let(:user) { create(:user, :admin) }
- let(:project) { create(:project, :public, :repository, description: 'Code and stuff', creator: user) }
-
- before do
- # We want vNext badge to be included and com/canary don't remove/hide any other elements.
- # This is why we're turning com and canary on by default for now.
- allow(Gitlab).to receive(:canary?).and_return(true)
- sign_in(user)
- end
-
- it "startup_css/project-#{type}.html" do
- get :show, params: {
- namespace_id: project.namespace.to_param,
- id: project
- }
-
- expect(response).to be_successful
- end
-
- it "startup_css/project-#{type}-signed-out.html" do
- sign_out(user)
-
- get :show, params: {
- namespace_id: project.namespace.to_param,
- id: project
- }
-
- expect(response).to be_successful
- end
-
- # This ensures that the correct css is generated for super sidebar
- it "startup_css/project-#{type}-super-sidebar.html" do
- user.update!(use_new_navigation: true)
-
- get :show, params: {
- namespace_id: project.namespace.to_param,
- id: project
- }
-
- expect(response).to be_successful
- end
- end
-
- describe ProjectsController, '(Startup CSS fixtures)', :saas, type: :controller do
- it_behaves_like 'startup css project fixtures', 'general'
- end
-
- describe ProjectsController, '(Startup CSS fixtures)', :saas, type: :controller do
- before do
- user.update!(theme_id: 11)
- end
-
- it_behaves_like 'startup css project fixtures', 'dark'
- end
-
- describe SessionsController, '(Startup CSS fixtures)', type: :controller do
- include DeviseHelpers
-
- before do
- set_devise_mapping(context: request)
- end
-
- it 'startup_css/sign-in.html' do
- get :new
-
- expect(response).to be_successful
- end
-
- it 'startup_css/sign-in-old.html' do
- stub_feature_flags(restyle_login_page: false)
-
- get :new
-
- expect(response).to be_successful
- end
- end
-end
diff --git a/spec/frontend/groups/components/overview_tabs_spec.js b/spec/frontend/groups/components/overview_tabs_spec.js
index 8db69295ac4..6bed744685f 100644
--- a/spec/frontend/groups/components/overview_tabs_spec.js
+++ b/spec/frontend/groups/components/overview_tabs_spec.js
@@ -362,9 +362,7 @@ describe('OverviewTabs', () => {
describe('when sort direction is changed', () => {
beforeEach(async () => {
await setup();
- await wrapper
- .findByRole('button', { name: 'Sorting Direction: Ascending' })
- .trigger('click');
+ await wrapper.findByRole('button', { name: 'Sort direction: Ascending' }).trigger('click');
});
it('updates query string with `sort` key', () => {
diff --git a/spec/frontend/groups/members/utils_spec.js b/spec/frontend/groups/members/utils_spec.js
index 0912e66e3e8..c7874b8b896 100644
--- a/spec/frontend/groups/members/utils_spec.js
+++ b/spec/frontend/groups/members/utils_spec.js
@@ -8,7 +8,19 @@ describe('group member utils', () => {
accessLevel: 50,
expires_at: '2020-10-16',
}),
- ).toEqual({ group_member: { access_level: 50, expires_at: '2020-10-16' } });
+ ).toEqual({
+ group_member: { access_level: 50, expires_at: '2020-10-16', member_role_id: null },
+ });
+
+ expect(
+ groupMemberRequestFormatter({
+ accessLevel: 50,
+ expires_at: '2020-10-16',
+ memberRoleId: 80,
+ }),
+ ).toEqual({
+ group_member: { access_level: 50, expires_at: '2020-10-16', member_role_id: 80 },
+ });
});
});
});
diff --git a/spec/frontend/header_spec.js b/spec/frontend/header_spec.js
index 4907dc09a3c..13c11863443 100644
--- a/spec/frontend/header_spec.js
+++ b/spec/frontend/header_spec.js
@@ -3,7 +3,14 @@ import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
import initTodoToggle, { initNavUserDropdownTracking } from '~/header';
import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
-describe('Header', () => {
+// TODO: Remove this with the removal of the old navigation.
+// See https://gitlab.com/groups/gitlab-org/-/epics/11875.
+//
+// This and ~/header will be removed. These tests no longer work due to the
+// corresponding fixtures changing for
+// https://gitlab.com/gitlab-org/gitlab/-/issues/420121.
+// eslint-disable-next-line jest/no-disabled-tests
+describe.skip('Header', () => {
describe('Todos notification', () => {
const todosPendingCount = '.js-todos-count';
diff --git a/spec/frontend/helpers/help_page_helper_spec.js b/spec/frontend/helpers/help_page_helper_spec.js
index 09c1a113a96..44a7300097f 100644
--- a/spec/frontend/helpers/help_page_helper_spec.js
+++ b/spec/frontend/helpers/help_page_helper_spec.js
@@ -3,6 +3,7 @@ import { helpPagePath } from '~/helpers/help_page_helper';
describe('help page helper', () => {
it.each`
relative_url_root | path | anchor | expected
+ ${undefined} | ${undefined} | ${undefined} | ${'/help'}
${undefined} | ${'administration/index'} | ${undefined} | ${'/help/administration/index'}
${''} | ${'administration/index'} | ${undefined} | ${'/help/administration/index'}
${'/'} | ${'administration/index'} | ${undefined} | ${'/help/administration/index'}
diff --git a/spec/frontend/helpers/init_simple_app_helper_spec.js b/spec/frontend/helpers/init_simple_app_helper_spec.js
index 7938e3851d0..de39a6f9d70 100644
--- a/spec/frontend/helpers/init_simple_app_helper_spec.js
+++ b/spec/frontend/helpers/init_simple_app_helper_spec.js
@@ -1,7 +1,9 @@
import { createWrapper } from '@vue/test-utils';
import Vue from 'vue';
+import VueApollo from 'vue-apollo';
import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import { initSimpleApp } from '~/helpers/init_simple_app_helper';
+import createDefaultClient from '~/lib/graphql';
const MockComponent = Vue.component('MockComponent', {
props: {
@@ -25,10 +27,10 @@ const findMock = () => wrapper.findComponent(MockComponent);
const didCreateApp = () => wrapper !== undefined;
-const initMock = (html, props = {}) => {
+const initMock = (html, options = {}) => {
setHTMLFixture(html);
- const app = initSimpleApp('#mount-here', MockComponent, { props });
+ const app = initSimpleApp('#mount-here', MockComponent, options);
wrapper = app ? createWrapper(app) : undefined;
};
@@ -58,4 +60,35 @@ describe('helpers/init_simple_app_helper/initSimpleApp', () => {
count: 123,
});
});
+
+ describe('options', () => {
+ describe('withApolloProvider', () => {
+ describe('if not true or not VueApollo', () => {
+ it('apolloProvider not created', () => {
+ initMock('<div id="mount-here"></div>', { withApolloProvider: false });
+
+ expect(wrapper.vm.$apollo).toBeUndefined();
+ });
+ });
+
+ describe('if true, creates default provider', () => {
+ it('creates a default apolloProvider', () => {
+ initMock('<div id="mount-here"></div>', { withApolloProvider: true });
+
+ expect(wrapper.vm.$apollo).not.toBeUndefined();
+ });
+ });
+
+ describe('if VueApollo, sets as default provider', () => {
+ it('uses the provided apolloClient', () => {
+ Vue.use(VueApollo);
+ const apolloProvider = new VueApollo({ defaultClient: createDefaultClient() });
+
+ initMock('<div id="mount-here"></div>', { withApolloProvider: apolloProvider });
+
+ expect(wrapper.vm.$apolloProvider).toBe(apolloProvider);
+ });
+ });
+ });
+ });
});
diff --git a/spec/frontend/ide/components/commit_sidebar/new_merge_request_option_spec.js b/spec/frontend/ide/components/commit_sidebar/new_merge_request_option_spec.js
index c5e540c3ea7..26c709e6951 100644
--- a/spec/frontend/ide/components/commit_sidebar/new_merge_request_option_spec.js
+++ b/spec/frontend/ide/components/commit_sidebar/new_merge_request_option_spec.js
@@ -4,7 +4,7 @@ import Vuex from 'vuex';
import { GlFormCheckbox } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import NewMergeRequestOption from '~/ide/components/commit_sidebar/new_merge_request_option.vue';
-import { createStore } from '~/ide/stores';
+import { createStoreOptions } from '~/ide/stores';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
Vue.use(Vuex);
@@ -22,17 +22,25 @@ describe('NewMergeRequestOption component', () => {
shouldDisableNewMrOption = false,
shouldCreateMR = false,
} = {}) => {
- store = createStore();
-
- wrapper = shallowMountExtended(NewMergeRequestOption, {
- store: {
- ...store,
- getters: {
- 'commit/shouldHideNewMrOption': shouldHideNewMrOption,
- 'commit/shouldDisableNewMrOption': shouldDisableNewMrOption,
- 'commit/shouldCreateMR': shouldCreateMR,
+ const storeOptions = createStoreOptions();
+
+ store = new Vuex.Store({
+ ...storeOptions,
+ modules: {
+ ...storeOptions.modules,
+ commit: {
+ ...storeOptions.modules.commit,
+ getters: {
+ shouldHideNewMrOption: () => shouldHideNewMrOption,
+ shouldDisableNewMrOption: () => shouldDisableNewMrOption,
+ shouldCreateMR: () => shouldCreateMR,
+ },
},
},
+ });
+
+ wrapper = shallowMountExtended(NewMergeRequestOption, {
+ store,
directives: {
GlTooltip: createMockDirective('gl-tooltip'),
},
diff --git a/spec/frontend/ide/components/jobs/detail/description_spec.js b/spec/frontend/ide/components/jobs/detail/description_spec.js
index 2bb0f3fccf4..909bd1f7c90 100644
--- a/spec/frontend/ide/components/jobs/detail/description_spec.js
+++ b/spec/frontend/ide/components/jobs/detail/description_spec.js
@@ -20,11 +20,8 @@ describe('IDE job description', () => {
});
it('renders CI icon', () => {
- expect(wrapper.find('.ci-status-icon').findComponent(GlIcon).exists()).toBe(true);
- });
-
- it('renders a borderless CI icon', () => {
- expect(wrapper.find('.borderless').findComponent(GlIcon).exists()).toBe(true);
+ expect(wrapper.find('[data-testid="ci-icon"]').findComponent(GlIcon).exists()).toBe(true);
+ expect(wrapper.find('[data-testid="status_success_borderless-icon"]').exists()).toBe(true);
});
it('renders bridge job details without the job link', () => {
diff --git a/spec/frontend/ide/components/jobs/item_spec.js b/spec/frontend/ide/components/jobs/item_spec.js
index ab442a27817..aa6fc5531dd 100644
--- a/spec/frontend/ide/components/jobs/item_spec.js
+++ b/spec/frontend/ide/components/jobs/item_spec.js
@@ -18,7 +18,7 @@ describe('IDE jobs item', () => {
});
it('renders CI icon', () => {
- expect(wrapper.find('[data-testid="status_success_borderless-icon"]').exists()).toBe(true);
+ expect(wrapper.find('[data-testid="ci-icon"]').exists()).toBe(true);
});
it('does not render view logs button if not started', async () => {
diff --git a/spec/frontend/ide/components/repo_editor_spec.js b/spec/frontend/ide/components/repo_editor_spec.js
index 33fa5bc799f..6f53aaed655 100644
--- a/spec/frontend/ide/components/repo_editor_spec.js
+++ b/spec/frontend/ide/components/repo_editor_spec.js
@@ -8,17 +8,14 @@ import { shallowMount } from '@vue/test-utils';
import waitForPromises from 'helpers/wait_for_promises';
import { stubPerformanceWebAPI } from 'helpers/performance';
import { exampleConfigs, exampleFiles } from 'jest/ide/lib/editorconfig/mock_data';
-import {
- EDITOR_CODE_INSTANCE_FN,
- EDITOR_DIFF_INSTANCE_FN,
- EXTENSION_CI_SCHEMA_FILE_NAME_MATCH,
-} from '~/editor/constants';
+import { EDITOR_CODE_INSTANCE_FN, EDITOR_DIFF_INSTANCE_FN } from '~/editor/constants';
import { EditorMarkdownExtension } from '~/editor/extensions/source_editor_markdown_ext';
import { EditorMarkdownPreviewExtension } from '~/editor/extensions/source_editor_markdown_livepreview_ext';
import { CiSchemaExtension } from '~/editor/extensions/source_editor_ci_schema_ext';
import SourceEditor from '~/editor/source_editor';
import RepoEditor from '~/ide/components/repo_editor.vue';
import { leftSidebarViews, FILE_VIEW_MODE_PREVIEW, viewerTypes } from '~/ide/constants';
+import { DEFAULT_CI_CONFIG_PATH } from '~/lib/utils/constants';
import ModelManager from '~/ide/lib/common/model_manager';
import service from '~/ide/services';
import { createStoreOptions } from '~/ide/stores';
@@ -56,7 +53,7 @@ const dummyFile = {
active: true,
},
ciConfig: {
- ...file(EXTENSION_CI_SCHEMA_FILE_NAME_MATCH),
+ ...file(DEFAULT_CI_CONFIG_PATH),
content: '',
tempFile: true,
active: true,
diff --git a/spec/frontend/import/details/components/bulk_import_details_app_spec.js b/spec/frontend/import/details/components/bulk_import_details_app_spec.js
new file mode 100644
index 00000000000..d32afb7ddcb
--- /dev/null
+++ b/spec/frontend/import/details/components/bulk_import_details_app_spec.js
@@ -0,0 +1,18 @@
+import { shallowMount } from '@vue/test-utils';
+import BulkImportDetailsApp from '~/import/details/components/bulk_import_details_app.vue';
+
+describe('Bulk import details app', () => {
+ let wrapper;
+
+ const createComponent = () => {
+ wrapper = shallowMount(BulkImportDetailsApp);
+ };
+
+ describe('template', () => {
+ it('renders heading', () => {
+ createComponent();
+
+ expect(wrapper.find('h1').text()).toBe('GitLab Migration details');
+ });
+ });
+});
diff --git a/spec/frontend/import/details/components/import_details_app_spec.js b/spec/frontend/import/details/components/import_details_app_spec.js
index 6e748a57a1d..cc3d9dd5e5e 100644
--- a/spec/frontend/import/details/components/import_details_app_spec.js
+++ b/spec/frontend/import/details/components/import_details_app_spec.js
@@ -12,7 +12,7 @@ describe('Import details app', () => {
it('renders heading', () => {
createComponent();
- expect(wrapper.find('h1').text()).toBe(ImportDetailsApp.i18n.pageTitle);
+ expect(wrapper.find('h1').text()).toBe('GitHub import details');
});
});
});
diff --git a/spec/frontend/import/details/components/import_details_table_spec.js b/spec/frontend/import/details/components/import_details_table_spec.js
index aee8573eb02..e2ba0ddad17 100644
--- a/spec/frontend/import/details/components/import_details_table_spec.js
+++ b/spec/frontend/import/details/components/import_details_table_spec.js
@@ -2,6 +2,7 @@ import { mount, shallowMount } from '@vue/test-utils';
import { GlEmptyState, GlLoadingIcon, GlTable } from '@gitlab/ui';
import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
+import { getParameterValues } from '~/lib/utils/url_utility';
import { HTTP_STATUS_OK, HTTP_STATUS_INTERNAL_SERVER_ERROR } from '~/lib/utils/http_status';
import { createAlert } from '~/alert';
import waitForPromises from 'helpers/wait_for_promises';
@@ -11,13 +12,32 @@ import ImportDetailsTable from '~/import/details/components/import_details_table
import { mockImportFailures, mockHeaders } from '../mock_data';
jest.mock('~/alert');
+jest.mock('~/lib/utils/url_utility', () => ({
+ ...jest.requireActual('~/lib/utils/url_utility'),
+ getParameterValues: jest.fn().mockReturnValue([]),
+}));
describe('Import details table', () => {
let wrapper;
let mock;
- const createComponent = ({ mountFn = shallowMount, provide = {} } = {}) => {
+ const mockFields = [
+ {
+ key: 'type',
+ label: 'Type',
+ },
+ {
+ key: 'title',
+ label: 'Title',
+ },
+ ];
+
+ const createComponent = ({ mountFn = shallowMount, props = {}, provide = {} } = {}) => {
wrapper = mountFn(ImportDetailsTable, {
+ propsData: {
+ fields: mockFields,
+ ...props,
+ },
provide,
});
};
@@ -109,5 +129,49 @@ describe('Import details table', () => {
});
});
});
+
+ describe('when bulk_import is true', () => {
+ const mockId = 144;
+ const mockEntityId = 68;
+
+ beforeEach(() => {
+ gon.api_version = 'v4';
+ getParameterValues.mockReturnValueOnce([mockId]);
+ getParameterValues.mockReturnValueOnce([mockEntityId]);
+
+ mock
+ .onGet(`/api/v4/bulk_imports/${mockId}/entities/${mockEntityId}/failures`)
+ .reply(HTTP_STATUS_OK, mockImportFailures, mockHeaders);
+
+ createComponent({
+ mountFn: mount,
+ props: {
+ bulkImport: true,
+ },
+ });
+ });
+
+ it('renders loading icon', () => {
+ expect(findGlLoadingIcon().exists()).toBe(true);
+ });
+
+ it('does not render loading icon after fetch', async () => {
+ await waitForPromises();
+
+ expect(findGlLoadingIcon().exists()).toBe(false);
+ });
+
+ it('sets items and pagination info', async () => {
+ await waitForPromises();
+
+ expect(findGlTableRows().length).toBe(mockImportFailures.length);
+ expect(findPaginationBar().props('pageInfo')).toMatchObject({
+ page: mockHeaders['x-page'],
+ perPage: mockHeaders['x-per-page'],
+ total: mockHeaders['x-total'],
+ totalPages: mockHeaders['x-total-pages'],
+ });
+ });
+ });
});
});
diff --git a/spec/frontend/import_entities/import_groups/components/import_status_spec.js b/spec/frontend/import_entities/import_groups/components/import_status_spec.js
new file mode 100644
index 00000000000..8d055d45dd8
--- /dev/null
+++ b/spec/frontend/import_entities/import_groups/components/import_status_spec.js
@@ -0,0 +1,99 @@
+import { GlBadge, GlLink } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+
+import ImportStatus from '~/import_entities/import_groups/components/import_status.vue';
+import { STATUSES, STATUS_ICON_MAP } from '~/import_entities/constants';
+
+describe('Group import status component', () => {
+ let wrapper;
+
+ const defaultProps = {
+ status: STATUSES.FINISHED,
+ };
+
+ const mockDetailsPath = '/details';
+
+ const createComponent = ({ props } = {}) => {
+ wrapper = shallowMount(ImportStatus, {
+ propsData: {
+ ...defaultProps,
+ ...props,
+ },
+ provide: {
+ detailsPath: mockDetailsPath,
+ },
+ });
+ };
+
+ const findGlBadge = () => wrapper.findComponent(GlBadge);
+ const findGlLink = () => wrapper.findComponent(GlLink);
+
+ describe('status badge text', () => {
+ describe('when import is partial', () => {
+ beforeEach(() => {
+ createComponent({
+ props: {
+ status: STATUSES.FINISHED,
+ hasFailures: true,
+ },
+ });
+ });
+
+ it('renders warning badge with text', () => {
+ expect(findGlBadge().props()).toMatchObject({
+ icon: 'status-alert',
+ variant: 'warning',
+ });
+ expect(findGlBadge().text()).toBe('Partially completed');
+ });
+ });
+
+ describe.each([
+ STATUSES.CREATED,
+ STATUSES.FAILED,
+ STATUSES.FINISHED,
+ STATUSES.STARTED,
+ STATUSES.TIMEOUT,
+ ])(`when import is %s`, (status) => {
+ beforeEach(() => {
+ createComponent({
+ props: {
+ status,
+ },
+ });
+ });
+
+ it('renders badge with text', () => {
+ const expectedStatus = STATUS_ICON_MAP[status];
+
+ expect(findGlBadge().props()).toMatchObject({
+ icon: expectedStatus.icon,
+ variant: expectedStatus.variant,
+ });
+ expect(findGlBadge().text()).toBe(expectedStatus.text);
+ });
+ });
+ });
+
+ describe('details link', () => {
+ it('does not render by default', () => {
+ createComponent();
+
+ expect(findGlLink().exists()).toBe(false);
+ });
+
+ it('renders with correct link when import is partial', () => {
+ createComponent({
+ props: {
+ id: 2,
+ entityId: 11,
+ hasFailures: true,
+ showDetailsLink: true,
+ status: STATUSES.FINISHED,
+ },
+ });
+
+ expect(findGlLink().attributes('href')).toBe('/details?id=2&entity_id=11');
+ });
+ });
+});
diff --git a/spec/frontend/import_entities/import_projects/components/github_organizations_box_spec.js b/spec/frontend/import_entities/import_projects/components/github_organizations_box_spec.js
index b6f96cd6a23..7b3758cbd25 100644
--- a/spec/frontend/import_entities/import_projects/components/github_organizations_box_spec.js
+++ b/spec/frontend/import_entities/import_projects/components/github_organizations_box_spec.js
@@ -1,8 +1,8 @@
import { GlCollapsibleListbox } from '@gitlab/ui';
import MockAdapter from 'axios-mock-adapter';
import { mount } from '@vue/test-utils';
-import { captureException } from '@sentry/browser';
import { nextTick } from 'vue';
+import { captureException } from '~/sentry/sentry_browser_wrapper';
import axios from '~/lib/utils/axios_utils';
import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
@@ -10,7 +10,7 @@ import { createAlert } from '~/alert';
import GithubOrganizationsBox from '~/import_entities/import_projects/components/github_organizations_box.vue';
-jest.mock('@sentry/browser');
+jest.mock('~/sentry/sentry_browser_wrapper');
jest.mock('~/alert');
const MOCK_RESPONSE = {
diff --git a/spec/frontend/incidents/components/incidents_list_spec.js b/spec/frontend/incidents/components/incidents_list_spec.js
index 470d63e7c2a..ff413c3feac 100644
--- a/spec/frontend/incidents/components/incidents_list_spec.js
+++ b/spec/frontend/incidents/components/incidents_list_spec.js
@@ -278,7 +278,7 @@ describe('Incidents List', () => {
${'severity'} | ${TH_SEVERITY_TEST_ID} | ${noneSort} | ${descSort} | ${ascSort}
${'status'} | ${TH_ESCALATION_STATUS_TEST_ID} | ${noneSort} | ${descSort} | ${ascSort}
${'publish date'} | ${TH_PUBLISHED_TEST_ID} | ${noneSort} | ${descSort} | ${ascSort}
- ${'due date'} | ${TH_INCIDENT_SLA_TEST_ID} | ${noneSort} | ${ascSort} | ${descSort}
+ ${'due date'} | ${TH_INCIDENT_SLA_TEST_ID} | ${noneSort} | ${descSort} | ${ascSort}
`(
'updates sort with new direction when sorting by $description',
async ({ selector, initialSort, firstSort, nextSort }) => {
diff --git a/spec/frontend/integrations/edit/components/dynamic_field_spec.js b/spec/frontend/integrations/edit/components/dynamic_field_spec.js
index e1d9aef752f..95d15eb2c00 100644
--- a/spec/frontend/integrations/edit/components/dynamic_field_spec.js
+++ b/spec/frontend/integrations/edit/components/dynamic_field_spec.js
@@ -1,16 +1,22 @@
import { GlFormGroup, GlFormCheckbox, GlFormInput, GlFormSelect, GlFormTextarea } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
+import Vue from 'vue';
+// eslint-disable-next-line no-restricted-imports
+import Vuex from 'vuex';
+
import DynamicField from '~/integrations/edit/components/dynamic_field.vue';
import { mockField } from '../mock_data';
+Vue.use(Vuex);
+
describe('DynamicField', () => {
let wrapper;
+ let store;
const createComponent = (props, isInheriting = false, editable = true) => {
- wrapper = mount(DynamicField, {
- propsData: { ...mockField, ...props },
- computed: {
+ store = new Vuex.Store({
+ getters: {
isInheriting: () => isInheriting,
propsSource: () => {
return {
@@ -19,6 +25,11 @@ describe('DynamicField', () => {
},
},
});
+
+ wrapper = mount(DynamicField, {
+ propsData: { ...mockField, ...props },
+ store,
+ });
};
const findGlFormGroup = () => wrapper.findComponent(GlFormGroup);
@@ -29,11 +40,11 @@ describe('DynamicField', () => {
describe('template', () => {
describe.each`
- isInheriting | editable | disabled | readonly | checkboxLabel
- ${true} | ${true} | ${'disabled'} | ${'readonly'} | ${undefined}
- ${false} | ${true} | ${undefined} | ${undefined} | ${'Custom checkbox label'}
- ${true} | ${false} | ${'disabled'} | ${'readonly'} | ${undefined}
- ${false} | ${false} | ${'disabled'} | ${undefined} | ${'Custom checkbox label'}
+ isInheriting | editable | disabled | readonly | checkboxLabel
+ ${true} | ${true} | ${'disabled'} | ${true} | ${undefined}
+ ${false} | ${true} | ${undefined} | ${false} | ${'Custom checkbox label'}
+ ${true} | ${false} | ${'disabled'} | ${true} | ${undefined}
+ ${false} | ${false} | ${'disabled'} | ${false} | ${'Custom checkbox label'}
`(
'dynamic field, when isInheriting = `$isInheriting` and editable = `$editable`',
({ isInheriting, editable, disabled, readonly, checkboxLabel }) => {
@@ -108,7 +119,7 @@ describe('DynamicField', () => {
it(`renders GlFormTextarea, which ${isInheriting ? 'is' : 'is not'} readonly`, () => {
expect(findGlFormTextarea().exists()).toBe(true);
- expect(findGlFormTextarea().find('textarea').attributes('readonly')).toBe(readonly);
+ expect('readonly' in findGlFormTextarea().find('textarea').attributes()).toBe(readonly);
});
it('does not render other types of input', () => {
@@ -132,7 +143,7 @@ describe('DynamicField', () => {
it(`renders GlFormInput, which ${isInheriting ? 'is' : 'is not'} readonly`, () => {
expect(findGlFormInput().exists()).toBe(true);
expect(findGlFormInput().attributes('type')).toBe('password');
- expect(findGlFormInput().attributes('readonly')).toBe(readonly);
+ expect('readonly' in findGlFormInput().attributes()).toBe(readonly);
});
it('does not render other types of input', () => {
@@ -161,9 +172,9 @@ describe('DynamicField', () => {
id: 'service_project_url',
name: 'service[project_url]',
placeholder: mockField.placeholder,
- required: 'required',
+ required: expect.any(String),
});
- expect(findGlFormInput().attributes('readonly')).toBe(readonly);
+ expect('readonly' in findGlFormInput().attributes()).toBe(readonly);
});
it('does not render other types of input', () => {
diff --git a/spec/frontend/integrations/edit/components/integration_form_spec.js b/spec/frontend/integrations/edit/components/integration_form_spec.js
index 5aa3ee35379..cef8fb0b720 100644
--- a/spec/frontend/integrations/edit/components/integration_form_spec.js
+++ b/spec/frontend/integrations/edit/components/integration_form_spec.js
@@ -2,7 +2,7 @@ import { GlAlert, GlForm } from '@gitlab/ui';
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import { nextTick } from 'vue';
-import * as Sentry from '@sentry/browser';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import { setHTMLFixture } from 'helpers/fixtures';
import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
@@ -29,7 +29,7 @@ import {
mockSectionJiraIssues,
} from '../mock_data';
-jest.mock('@sentry/browser');
+jest.mock('~/sentry/sentry_browser_wrapper');
jest.mock('~/lib/utils/url_utility');
describe('IntegrationForm', () => {
diff --git a/spec/frontend/integrations/overrides/components/integration_overrides_spec.js b/spec/frontend/integrations/overrides/components/integration_overrides_spec.js
index 9e863eaecfd..ac67d53e00d 100644
--- a/spec/frontend/integrations/overrides/components/integration_overrides_spec.js
+++ b/spec/frontend/integrations/overrides/components/integration_overrides_spec.js
@@ -1,7 +1,7 @@
import { GlTable, GlLink, GlPagination, GlAlert } from '@gitlab/ui';
-import * as Sentry from '@sentry/browser';
import { shallowMount, mount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import waitForPromises from 'helpers/wait_for_promises';
import { DEFAULT_PER_PAGE } from '~/api';
import IntegrationOverrides from '~/integrations/overrides/components/integration_overrides.vue';
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 cfc2fd65cc1..19b7fad5fc8 100644
--- a/spec/frontend/invite_members/components/invite_members_modal_spec.js
+++ b/spec/frontend/invite_members/components/invite_members_modal_spec.js
@@ -1,4 +1,4 @@
-import { GlModal, GlSprintf, GlFormGroup, GlCollapse, GlIcon } from '@gitlab/ui';
+import { GlLink, GlModal, GlSprintf, GlFormGroup, GlCollapse, GlIcon } from '@gitlab/ui';
import MockAdapter from 'axios-mock-adapter';
import { nextTick } from 'vue';
import { stubComponent } from 'helpers/stub_component';
@@ -60,6 +60,7 @@ describe('InviteMembersModal', () => {
let mock;
let trackingSpy;
const showToast = jest.fn();
+ const newUsersUrl = '/new/users/url';
const expectTracking = (action, label = undefined, property = undefined) =>
expect(trackingSpy).toHaveBeenCalledWith(INVITE_MEMBER_MODAL_TRACKING_CATEGORY, action, {
@@ -68,11 +69,13 @@ describe('InviteMembersModal', () => {
property,
});
- const createComponent = (props = {}, stubs = {}) => {
+ const createComponent = (props = {}, stubs = {}, provide = {}) => {
wrapper = shallowMountExtended(InviteMembersModal, {
provide: {
newProjectPath,
name: propsData.name,
+ newUsersUrl,
+ ...provide,
},
propsData: {
usersLimitDataset: {},
@@ -129,6 +132,7 @@ describe('InviteMembersModal', () => {
const findEmptyInvitesAlert = () => wrapper.findByTestId('empty-invites-alert');
const findMemberErrorAlert = () => wrapper.findByTestId('alert-member-error');
const findMoreInviteErrorsButton = () => wrapper.findByTestId('accordion-button');
+ const findEmailSignupDisabledAlert = () => wrapper.findByTestId('email-signup-disabled-alert');
const findUserLimitAlert = () => wrapper.findComponent(UserLimitNotification);
const findAccordion = () => wrapper.findComponent(GlCollapse);
const findErrorsIcon = () => wrapper.findComponent(GlIcon);
@@ -759,6 +763,58 @@ describe('InviteMembersModal', () => {
expect(findMemberErrorAlert().exists()).toBe(false);
});
});
+
+ describe('when email signup is not allowed', () => {
+ beforeEach(() => {
+ createComponent({}, {}, { isEmailSignupEnabled: false });
+ });
+
+ it('shows the correct form description', () => {
+ expect(membersFormGroupDescription()).toBe('Select members');
+ });
+
+ it('shows an alert', () => {
+ expect(findEmailSignupDisabledAlert().text()).toBe(
+ "Administrators can add new users by email manually. After they've been added, you can invite them to this group with their username.",
+ );
+ });
+
+ it('does not render a link', () => {
+ expect(findEmailSignupDisabledAlert().findComponent(GlLink).exists()).toBe(false);
+ });
+
+ describe('when the current user is an admin', () => {
+ beforeEach(() => {
+ createComponent({}, {}, { isCurrentUserAdmin: true, isEmailSignupEnabled: false });
+ });
+
+ it('shows an alert', () => {
+ expect(findEmailSignupDisabledAlert().text()).toBe(
+ "Administrators can add new users by email manually. After they've been added, you can invite them to this group with their username.",
+ );
+ });
+
+ it('renders a link', () => {
+ expect(findEmailSignupDisabledAlert().findComponent(GlLink).attributes('href')).toBe(
+ newUsersUrl,
+ );
+ });
+
+ describe('when no new users url is provided', () => {
+ beforeEach(() => {
+ createComponent(
+ {},
+ {},
+ { isCurrentUserAdmin: true, isEmailSignupEnabled: false, newUsersUrl: '' },
+ );
+ });
+
+ it('does not render a link', () => {
+ expect(findEmailSignupDisabledAlert().findComponent(GlLink).exists()).toBe(false);
+ });
+ });
+ });
+ });
});
describe('when inviting members and non-members in same click', () => {
diff --git a/spec/frontend/invite_members/components/members_token_select_spec.js b/spec/frontend/invite_members/components/members_token_select_spec.js
index 925534edd7c..a4b8a8b0197 100644
--- a/spec/frontend/invite_members/components/members_token_select_spec.js
+++ b/spec/frontend/invite_members/components/members_token_select_spec.js
@@ -157,6 +157,21 @@ describe('MembersTokenSelect', () => {
expect(tokenSelector.props('allowUserDefinedTokens')).toBe(result);
});
+
+ describe('when cannot use email token', () => {
+ beforeEach(() => {
+ wrapper = createComponent({ canUseEmailToken: false });
+ tokenSelector = findTokenSelector();
+
+ tokenSelector.vm.$emit('text-input', 'foo@bar.com');
+
+ return nextTick();
+ });
+
+ it('does not allow user defined tokens', () => {
+ expect(tokenSelector.props('allowUserDefinedTokens')).toBe(false);
+ });
+ });
});
});
diff --git a/spec/frontend/invite_members/utils/member_utils_spec.js b/spec/frontend/invite_members/utils/member_utils_spec.js
index abae43c3dbb..4d71a35ff99 100644
--- a/spec/frontend/invite_members/utils/member_utils_spec.js
+++ b/spec/frontend/invite_members/utils/member_utils_spec.js
@@ -1,4 +1,8 @@
-import { memberName, triggerExternalAlert } from '~/invite_members/utils/member_utils';
+import {
+ memberName,
+ triggerExternalAlert,
+ inviteMembersTrackingOptions,
+} from '~/invite_members/utils/member_utils';
jest.mock('~/lib/utils/url_utility');
@@ -18,3 +22,13 @@ describe('Trigger External Alert', () => {
expect(triggerExternalAlert()).toBe(false);
});
});
+
+describe('inviteMembersTrackingOptions', () => {
+ it('returns options with a label', () => {
+ expect(inviteMembersTrackingOptions({ label: '_label_' })).toEqual({ label: '_label_' });
+ });
+
+ it('handles options that has no label', () => {
+ expect(inviteMembersTrackingOptions({})).toEqual({ label: undefined });
+ });
+});
diff --git a/spec/frontend/issuable/components/locked_badge_spec.js b/spec/frontend/issuable/components/locked_badge_spec.js
index 73ab6e36ba1..46143d16712 100644
--- a/spec/frontend/issuable/components/locked_badge_spec.js
+++ b/spec/frontend/issuable/components/locked_badge_spec.js
@@ -39,7 +39,7 @@ describe('LockedBadge component', () => {
it('has title', () => {
expect(findBadge().attributes('title')).toBe(
- 'This issue is locked. Only project members can comment.',
+ 'The discussion in this issue is locked. Only project members can comment.',
);
});
});
diff --git a/spec/frontend/issuable/components/status_badge_spec.js b/spec/frontend/issuable/components/status_badge_spec.js
index cdc848626c7..9ab5b4f7149 100644
--- a/spec/frontend/issuable/components/status_badge_spec.js
+++ b/spec/frontend/issuable/components/status_badge_spec.js
@@ -16,10 +16,10 @@ describe('StatusBadge component', () => {
${'merge_request'} | ${'Open'} | ${'opened'} | ${'success'} | ${'merge-request-open'}
${'merge_request'} | ${'Closed'} | ${'closed'} | ${'danger'} | ${'merge-request-close'}
${'merge_request'} | ${'Merged'} | ${'merged'} | ${'info'} | ${'merge'}
- ${'issue'} | ${'Open'} | ${'opened'} | ${'success'} | ${'issues'}
- ${'issue'} | ${'Closed'} | ${'closed'} | ${'info'} | ${'issue-closed'}
- ${'epic'} | ${'Open'} | ${'opened'} | ${'success'} | ${'epic'}
- ${'epic'} | ${'Closed'} | ${'closed'} | ${'info'} | ${'epic-closed'}
+ ${'issue'} | ${'Open'} | ${'opened'} | ${'success'} | ${'issue-open-m'}
+ ${'issue'} | ${'Closed'} | ${'closed'} | ${'info'} | ${'issue-close'}
+ ${'epic'} | ${'Open'} | ${'opened'} | ${'success'} | ${'issue-open-m'}
+ ${'epic'} | ${'Closed'} | ${'closed'} | ${'info'} | ${'issue-close'}
`(
'when issuableType=$issuableType and state=$state',
({ issuableType, badgeText, state, badgeVariant, badgeIcon }) => {
diff --git a/spec/frontend/issues/dashboard/components/issues_dashboard_app_spec.js b/spec/frontend/issues/dashboard/components/issues_dashboard_app_spec.js
index f6c9fab76d1..35699568793 100644
--- a/spec/frontend/issues/dashboard/components/issues_dashboard_app_spec.js
+++ b/spec/frontend/issues/dashboard/components/issues_dashboard_app_spec.js
@@ -1,9 +1,9 @@
import { GlDisclosureDropdown, GlEmptyState } from '@gitlab/ui';
-import * as Sentry from '@sentry/browser';
import AxiosMockAdapter from 'axios-mock-adapter';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import { cloneDeep } from 'lodash';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import getIssuesQuery from 'ee_else_ce/issues/dashboard/queries/get_issues.query.graphql';
import IssueCardStatistics from 'ee_else_ce/issues/list/components/issue_card_statistics.vue';
import IssueCardTimeInfo from 'ee_else_ce/issues/list/components/issue_card_time_info.vue';
@@ -45,7 +45,7 @@ import {
issuesQueryResponse,
} from '../mock_data';
-jest.mock('@sentry/browser');
+jest.mock('~/sentry/sentry_browser_wrapper');
jest.mock('~/lib/utils/scroll_utils', () => ({ scrollUp: jest.fn() }));
describe('IssuesDashboardApp component', () => {
diff --git a/spec/frontend/issues/issue_spec.js b/spec/frontend/issues/issue_spec.js
index bf2ca42f71f..b976a051f7a 100644
--- a/spec/frontend/issues/issue_spec.js
+++ b/spec/frontend/issues/issue_spec.js
@@ -58,7 +58,17 @@ describe('Issue', () => {
);
});
- it('updates issueCounter text', () => {
+ // TODO: Remove this with the removal of the old navigation.
+ // See https://gitlab.com/groups/gitlab-org/-/epics/11875.
+ // See also https://gitlab.com/gitlab-org/gitlab/-/issues/429678 about
+ // reimplementing this in the new navigation.
+ //
+ // Since this entire suite only tests the issue count updating, removing
+ // this test would mean removing the entire suite. But, ~/issues/issue.js
+ // does more than just that. Tests should be written to cover those other
+ // features. So we're just skipping this for now.
+ // eslint-disable-next-line jest/no-disabled-tests
+ it.skip('updates issueCounter text', () => {
expect(testContext.issueCounter).toBeVisible();
expect(testContext.issueCounter).toHaveText(expectedCounterText);
});
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 f830168ce5d..6bd952cd215 100644
--- a/spec/frontend/issues/list/components/issues_list_app_spec.js
+++ b/spec/frontend/issues/list/components/issues_list_app_spec.js
@@ -1,11 +1,11 @@
import { GlButton, GlDisclosureDropdown, GlDrawer } from '@gitlab/ui';
-import * as Sentry from '@sentry/browser';
import { mount, shallowMount } from '@vue/test-utils';
import AxiosMockAdapter from 'axios-mock-adapter';
import { cloneDeep } from 'lodash';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import VueRouter from 'vue-router';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
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';
@@ -87,7 +87,7 @@ import {
import('~/issuable');
import('~/users_select');
-jest.mock('@sentry/browser');
+jest.mock('~/sentry/sentry_browser_wrapper');
jest.mock('~/alert');
jest.mock('~/lib/utils/scroll_utils', () => ({ scrollUp: jest.fn() }));
diff --git a/spec/frontend/issues/service_desk/components/service_desk_list_app_spec.js b/spec/frontend/issues/service_desk/components/service_desk_list_app_spec.js
index d28b4f2fe76..bb388cefa95 100644
--- a/spec/frontend/issues/service_desk/components/service_desk_list_app_spec.js
+++ b/spec/frontend/issues/service_desk/components/service_desk_list_app_spec.js
@@ -3,8 +3,8 @@ import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import { cloneDeep } from 'lodash';
import VueRouter from 'vue-router';
-import * as Sentry from '@sentry/browser';
import AxiosMockAdapter from 'axios-mock-adapter';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import axios from '~/lib/utils/axios_utils';
import createMockApollo from 'helpers/mock_apollo_helper';
import setWindowLocation from 'helpers/set_window_location_helper';
@@ -55,7 +55,7 @@ import {
locationSearch,
} from '../mock_data';
-jest.mock('@sentry/browser');
+jest.mock('~/sentry/sentry_browser_wrapper');
jest.mock('~/alert');
jest.mock('~/lib/utils/scroll_utils', () => ({ scrollUp: jest.fn() }));
diff --git a/spec/frontend/issues/show/components/header_actions_spec.js b/spec/frontend/issues/show/components/header_actions_spec.js
index e508045eff3..d0c2a1a5f1b 100644
--- a/spec/frontend/issues/show/components/header_actions_spec.js
+++ b/spec/frontend/issues/show/components/header_actions_spec.js
@@ -1,9 +1,16 @@
import Vue, { nextTick } from 'vue';
-import { GlDropdown, GlDropdownItem, GlLink, GlModal, GlButton } from '@gitlab/ui';
+import {
+ GlDisclosureDropdown,
+ GlDisclosureDropdownItem,
+ GlLink,
+ GlModal,
+ GlButton,
+} from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
// eslint-disable-next-line no-restricted-imports
import Vuex from 'vuex';
import VueApollo from 'vue-apollo';
+import { stubComponent } from 'helpers/stub_component';
import waitForPromises from 'helpers/wait_for_promises';
import { mockTracking } from 'helpers/tracking_helper';
import { createAlert, VARIANT_SUCCESS } from '~/alert';
@@ -120,8 +127,10 @@ describe('HeaderActions component', () => {
const findDropdownBy = (dataTestId) => wrapper.find(`[data-testid="${dataTestId}"]`);
const findMobileDropdown = () => findDropdownBy('mobile-dropdown');
const findDesktopDropdown = () => findDropdownBy('desktop-dropdown');
- const findMobileDropdownItems = () => findMobileDropdown().findAllComponents(GlDropdownItem);
- const findDesktopDropdownItems = () => findDesktopDropdown().findAllComponents(GlDropdownItem);
+ const findMobileDropdownItems = () =>
+ findMobileDropdown().findAllComponents(GlDisclosureDropdownItem);
+ const findDesktopDropdownItems = () =>
+ findDesktopDropdown().findAllComponents(GlDisclosureDropdownItem);
const findAbuseCategorySelector = () => wrapper.findComponent(AbuseCategorySelector);
const findReportAbuseButton = () => wrapper.find(`[data-testid="report-abuse-item"]`);
const findNotificationWidget = () => wrapper.find(`[data-testid="notification-toggle"]`);
@@ -179,6 +188,11 @@ describe('HeaderActions component', () => {
},
stubs: {
GlButton,
+ GlDisclosureDropdown: stubComponent(GlDisclosureDropdown, {
+ methods: {
+ close: jest.fn(),
+ },
+ }),
},
});
};
@@ -217,7 +231,7 @@ describe('HeaderActions component', () => {
});
it('calls apollo mutation', () => {
- findToggleIssueStateButton().vm.$emit('click');
+ findToggleIssueStateButton().vm.$emit('action');
expect(updateIssueMutationResponseHandler).toHaveBeenCalledWith({
input: {
@@ -229,7 +243,7 @@ describe('HeaderActions component', () => {
});
it('dispatches a custom event to update the issue page', async () => {
- findToggleIssueStateButton().vm.$emit('click');
+ findToggleIssueStateButton().vm.$emit('action');
await waitForPromises();
@@ -286,7 +300,11 @@ describe('HeaderActions component', () => {
it(`${isItemVisible ? 'shows' : 'hides'} "${itemText}" item`, () => {
expect(
findDropdownItems()
- .filter((item) => item.text() === itemText)
+ .filter((item) => {
+ return item.props('item')
+ ? item.props('item').text === itemText
+ : item.text() === itemText;
+ })
.exists(),
).toBe(isItemVisible);
});
@@ -313,7 +331,7 @@ describe('HeaderActions component', () => {
it('should trigger "open.form" event when clicked', async () => {
expect(issuesEventHub.$emit).not.toHaveBeenCalled();
- await findEditButton().trigger('click');
+ await findEditButton().vm.$emit('click');
expect(issuesEventHub.$emit).toHaveBeenCalledWith('open.form');
});
});
@@ -328,7 +346,7 @@ describe('HeaderActions component', () => {
});
it('tracks clicking on button', () => {
- findDesktopDropdownItems().at(4).vm.$emit('click');
+ findDesktopDropdownItems().at(4).vm.$emit('action');
expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_dropdown', {
label: 'delete_issue',
@@ -345,7 +363,7 @@ describe('HeaderActions component', () => {
promoteToEpicHandler: promoteToEpicMutationSuccessResponseHandler,
});
- wrapper.find('[data-testid="promote-button"]').vm.$emit('click');
+ wrapper.find('[data-testid="promote-button"]').vm.$emit('action');
await waitForPromises();
});
@@ -381,7 +399,7 @@ describe('HeaderActions component', () => {
promoteToEpicHandler: promoteToEpicMutationErrorHandler,
});
- wrapper.find('[data-testid="promote-button"]').vm.$emit('click');
+ wrapper.find('[data-testid="promote-button"]').vm.$emit('action');
await waitForPromises();
});
@@ -483,7 +501,7 @@ describe('HeaderActions component', () => {
});
it('opens the abuse category drawer', async () => {
- findReportAbuseButton().vm.$emit('click');
+ findReportAbuseButton().vm.$emit('action');
await nextTick();
@@ -491,7 +509,7 @@ describe('HeaderActions component', () => {
});
it('closes the abuse category drawer', async () => {
- await findReportAbuseButton().vm.$emit('click');
+ await findReportAbuseButton().vm.$emit('action');
expect(findAbuseCategorySelector().exists()).toEqual(true);
await findAbuseCategorySelector().vm.$emit('close-drawer');
@@ -603,7 +621,7 @@ describe('HeaderActions component', () => {
});
it('shows toast message', () => {
- findCopyRefenceDropdownItem().vm.$emit('click');
+ findCopyRefenceDropdownItem().vm.$emit('action');
expect(toast).toHaveBeenCalledWith('Reference copied');
});
@@ -652,7 +670,7 @@ describe('HeaderActions component', () => {
});
it('shows toast message', () => {
- findCopyEmailItem().vm.$emit('click');
+ findCopyEmailItem().vm.$emit('action');
expect(toast).toHaveBeenCalledWith('Email address copied');
});
@@ -710,11 +728,13 @@ describe('HeaderActions component', () => {
props: { issueType, issuableEmailAddress: 'mock-email-address' },
});
- expect(wrapper.findComponent(GlDropdown).attributes('text')).toBe(
+ expect(wrapper.findComponent(GlDisclosureDropdown).props('toggleText')).toBe(
`${capitalizeFirstCharacter(expectedText)} actions`,
);
expect(findDropdownBy('copy-email').text()).toBe(`Copy ${expectedText} email address`);
- expect(findDesktopDropdownItems().at(1).text()).toBe(`New related ${expectedText}`);
+ expect(findDesktopDropdownItems().at(1).props('item').text).toBe(
+ `New related ${expectedText}`,
+ );
});
});
});
diff --git a/spec/frontend/issues/show/components/issue_header_spec.js b/spec/frontend/issues/show/components/issue_header_spec.js
index 6acc7004576..6c4e357d722 100644
--- a/spec/frontend/issues/show/components/issue_header_spec.js
+++ b/spec/frontend/issues/show/components/issue_header_spec.js
@@ -47,7 +47,7 @@ describe('IssueHeader component', () => {
issuableType: 'issue',
serviceDeskReplyTo: '',
showWorkItemTypeIcon: true,
- statusIcon: 'issues',
+ statusIcon: 'issue-open-m',
workspaceType: 'project',
});
});
@@ -63,7 +63,7 @@ describe('IssueHeader component', () => {
});
it('renders correct icon', () => {
- expect(findIssuableHeader().props('statusIcon')).toBe('issues');
+ expect(findIssuableHeader().props('statusIcon')).toBe('issue-open-m');
});
});
@@ -77,7 +77,7 @@ describe('IssueHeader component', () => {
});
it('renders correct icon', () => {
- expect(findIssuableHeader().props('statusIcon')).toBe('issue-closed');
+ expect(findIssuableHeader().props('statusIcon')).toBe('issue-close');
});
describe('when issue is marked as duplicate', () => {
diff --git a/spec/frontend/issues/show/components/sticky_header_spec.js b/spec/frontend/issues/show/components/sticky_header_spec.js
index a909084956f..43d96f398b6 100644
--- a/spec/frontend/issues/show/components/sticky_header_spec.js
+++ b/spec/frontend/issues/show/components/sticky_header_spec.js
@@ -36,12 +36,12 @@ describe('StickyHeader component', () => {
it.each`
issuableType | issuableStatus | statusIcon
- ${TYPE_INCIDENT} | ${STATUS_OPEN} | ${'issues'}
- ${TYPE_INCIDENT} | ${STATUS_CLOSED} | ${'issue-closed'}
- ${TYPE_ISSUE} | ${STATUS_OPEN} | ${'issues'}
- ${TYPE_ISSUE} | ${STATUS_CLOSED} | ${'issue-closed'}
- ${TYPE_EPIC} | ${STATUS_OPEN} | ${'epic'}
- ${TYPE_EPIC} | ${STATUS_CLOSED} | ${'epic-closed'}
+ ${TYPE_INCIDENT} | ${STATUS_OPEN} | ${'issue-open-m'}
+ ${TYPE_INCIDENT} | ${STATUS_CLOSED} | ${'issue-close'}
+ ${TYPE_ISSUE} | ${STATUS_OPEN} | ${'issue-open-m'}
+ ${TYPE_ISSUE} | ${STATUS_CLOSED} | ${'issue-close'}
+ ${TYPE_EPIC} | ${STATUS_OPEN} | ${'issue-open-m'}
+ ${TYPE_EPIC} | ${STATUS_CLOSED} | ${'issue-close'}
`(
'shows with state icon "$statusIcon" for $issuableType when status is $issuableStatus',
({ issuableType, issuableStatus, statusIcon }) => {
diff --git a/spec/frontend/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/setup_instructions_spec.js b/spec/frontend/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/setup_instructions_spec.js
index 40ea6058c70..efe89100e90 100644
--- a/spec/frontend/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/setup_instructions_spec.js
+++ b/spec/frontend/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/setup_instructions_spec.js
@@ -1,15 +1,23 @@
+import { nextTick } from 'vue';
import { shallowMount } from '@vue/test-utils';
-import { GlButton, GlLink } from '@gitlab/ui';
+import { GlButton, GlFormCheckbox, GlLink } from '@gitlab/ui';
-import { OAUTH_SELF_MANAGED_DOC_LINK } from '~/jira_connect/subscriptions/constants';
+import {
+ PREREQUISITES_DOC_LINK,
+ OAUTH_SELF_MANAGED_DOC_LINK,
+ SET_UP_INSTANCE_DOC_LINK,
+} from '~/jira_connect/subscriptions/constants';
import SetupInstructions from '~/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/setup_instructions.vue';
describe('SetupInstructions', () => {
let wrapper;
- const findGlLink = () => wrapper.findComponent(GlLink);
+ const findPrerequisitesGlLink = () => wrapper.findAllComponents(GlLink).at(0);
+ const findOAuthGlLink = () => wrapper.findAllComponents(GlLink).at(1);
+ const findSetUpInstanceGlLink = () => wrapper.findAllComponents(GlLink).at(2);
const findBackButton = () => wrapper.findAllComponents(GlButton).at(0);
const findNextButton = () => wrapper.findAllComponents(GlButton).at(1);
+ const findCheckboxAtIndex = (index) => wrapper.findAllComponents(GlFormCheckbox).at(index);
const createComponent = () => {
wrapper = shallowMount(SetupInstructions);
@@ -20,8 +28,34 @@ describe('SetupInstructions', () => {
createComponent();
});
- it('renders "Learn more" link to documentation', () => {
- expect(findGlLink().attributes('href')).toBe(OAUTH_SELF_MANAGED_DOC_LINK);
+ it('renders "Prerequisites" link to documentation', () => {
+ expect(findPrerequisitesGlLink().attributes('href')).toBe(PREREQUISITES_DOC_LINK);
+ });
+
+ it('renders "Set up OAuth authentication" link to documentation', () => {
+ expect(findOAuthGlLink().attributes('href')).toBe(OAUTH_SELF_MANAGED_DOC_LINK);
+ });
+
+ it('renders "Set up your instance" link to documentation', () => {
+ expect(findSetUpInstanceGlLink().attributes('href')).toBe(SET_UP_INSTANCE_DOC_LINK);
+ });
+
+ describe('NextButton', () => {
+ it('emits next event when clicked and all steps checked', async () => {
+ createComponent();
+
+ findCheckboxAtIndex(0).vm.$emit('input', true);
+ findCheckboxAtIndex(1).vm.$emit('input', true);
+ findCheckboxAtIndex(2).vm.$emit('input', true);
+
+ await nextTick();
+
+ expect(findNextButton().attributes('disabled')).toBeUndefined();
+ });
+
+ it('disables button when not all steps are checked', () => {
+ expect(findNextButton().attributes('disabled')).toBe('true');
+ });
});
describe('when "Next" button is clicked', () => {
diff --git a/spec/frontend/lib/utils/color_utils_spec.js b/spec/frontend/lib/utils/color_utils_spec.js
index 92ac66c19f0..aac50a2c850 100644
--- a/spec/frontend/lib/utils/color_utils_spec.js
+++ b/spec/frontend/lib/utils/color_utils_spec.js
@@ -18,15 +18,17 @@ describe('Color utils', () => {
describe('darkModeEnabled', () => {
it.each`
- page | bodyClass | ideTheme | expected
+ page | rootClass | ideTheme | expected
${'ide:index'} | ${'gl-dark'} | ${'monokai-light'} | ${false}
${'ide:index'} | ${'ui-light'} | ${'monokai'} | ${true}
${'groups:issues:index'} | ${'ui-light'} | ${'monokai'} | ${false}
${'groups:issues:index'} | ${'gl-dark'} | ${'monokai-light'} | ${true}
`(
- 'is $expected on $page with $bodyClass body class and $ideTheme IDE theme',
- ({ page, bodyClass, ideTheme, expected }) => {
- document.body.outerHTML = `<body class="${bodyClass}" data-page="${page}"></body>`;
+ 'is $expected on $page with $rootClass root class and $ideTheme IDE theme',
+ ({ page, rootClass, ideTheme, expected }) => {
+ document.documentElement.className = rootClass;
+ document.body.outerHTML = `<body data-page="${page}"></body>`;
+
window.gon = {
user_color_scheme: ideTheme,
};
diff --git a/spec/frontend/lib/utils/common_utils_spec.js b/spec/frontend/lib/utils/common_utils_spec.js
index 8697249ebf5..6295914b127 100644
--- a/spec/frontend/lib/utils/common_utils_spec.js
+++ b/spec/frontend/lib/utils/common_utils_spec.js
@@ -1213,4 +1213,28 @@ describe('common_utils', () => {
expect(cloned.ref === ref).toBe(false);
});
});
+
+ describe('isDefaultCiConfig', () => {
+ it('returns true when the path is the default CI config path', () => {
+ expect(commonUtils.isDefaultCiConfig('.gitlab-ci.yml')).toBe(true);
+ });
+
+ it('returns false when the path is not the default CI config path', () => {
+ expect(commonUtils.isDefaultCiConfig('some/other/path.yml')).toBe(false);
+ });
+ });
+
+ describe('hasCiConfigExtension', () => {
+ it('returns true when the path is the default CI config path', () => {
+ expect(commonUtils.hasCiConfigExtension('.gitlab-ci.yml')).toBe(true);
+ });
+
+ it('returns true when the path has a CI config extension', () => {
+ expect(commonUtils.hasCiConfigExtension('some/path.gitlab-ci.yml')).toBe(true);
+ });
+
+ it('returns false when the path does not have a CI config extension', () => {
+ expect(commonUtils.hasCiConfigExtension('some/other/path.yml')).toBe(false);
+ });
+ });
});
diff --git a/spec/frontend/lib/utils/datetime/timeago_utility_spec.js b/spec/frontend/lib/utils/datetime/timeago_utility_spec.js
index 74ce8175357..44db4cf88a2 100644
--- a/spec/frontend/lib/utils/datetime/timeago_utility_spec.js
+++ b/spec/frontend/lib/utils/datetime/timeago_utility_spec.js
@@ -160,5 +160,24 @@ describe('TimeAgo utils', () => {
);
},
);
+
+ describe('With User Setting Time Format', () => {
+ it.each`
+ timeDisplayFormat | display | text
+ ${0} | ${'System'} | ${'Feb 18, 2020, 10:22 PM'}
+ ${1} | ${'12-hour'} | ${'Feb 18, 2020, 10:22 PM'}
+ ${2} | ${'24-hour'} | ${'Feb 18, 2020, 22:22'}
+ `(`'$display' renders as '$text'`, ({ timeDisplayFormat, text }) => {
+ gon.time_display_relative = false;
+ gon.time_display_format = timeDisplayFormat;
+
+ const element = document.querySelector('time');
+ localTimeAgo([element]);
+
+ jest.runAllTimers();
+
+ expect(element.innerText).toBe(text);
+ });
+ });
});
});
diff --git a/spec/frontend/lib/utils/forms_spec.js b/spec/frontend/lib/utils/forms_spec.js
index b97f5bf3c51..88b1f9afdf5 100644
--- a/spec/frontend/lib/utils/forms_spec.js
+++ b/spec/frontend/lib/utils/forms_spec.js
@@ -6,7 +6,8 @@ import {
hasMinimumLength,
isParseableAsInteger,
isIntegerGreaterThan,
- isEmail,
+ isServiceDeskSettingEmail,
+ isUserEmail,
parseRailsFormFields,
} from '~/lib/utils/forms';
@@ -202,7 +203,7 @@ describe('lib/utils/forms', () => {
);
});
- describe('isEmail', () => {
+ describe('isServiceDeskSettingEmail', () => {
it.each`
input | returnValue
${'user-with_special-chars@example.com'} | ${true}
@@ -219,7 +220,28 @@ describe('lib/utils/forms', () => {
${' '} | ${false}
${'12'} | ${false}
`('returns $returnValue for value $input', ({ input, returnValue }) => {
- expect(isEmail(input)).toBe(returnValue);
+ expect(isServiceDeskSettingEmail(input)).toBe(returnValue);
+ });
+ });
+
+ describe('isUserEmail', () => {
+ it.each`
+ input | returnValue
+ ${'user-with_special-chars@example.com'} | ${true}
+ ${'user@subdomain.example.com'} | ${true}
+ ${'user@example.com'} | ${true}
+ ${'user@example.co'} | ${true}
+ ${'user@example.c'} | ${true}
+ ${'user@example'} | ${true}
+ ${''} | ${false}
+ ${[]} | ${false}
+ ${null} | ${false}
+ ${undefined} | ${false}
+ ${'hello'} | ${false}
+ ${' '} | ${false}
+ ${'12'} | ${false}
+ `('returns $returnValue for value $input', ({ input, returnValue }) => {
+ expect(isUserEmail(input)).toBe(returnValue);
});
});
diff --git a/spec/frontend/members/components/avatars/group_avatar_spec.js b/spec/frontend/members/components/avatars/group_avatar_spec.js
index 8e4263f88fe..1463aa5ae59 100644
--- a/spec/frontend/members/components/avatars/group_avatar_spec.js
+++ b/spec/frontend/members/components/avatars/group_avatar_spec.js
@@ -1,8 +1,9 @@
-import { GlAvatarLink } from '@gitlab/ui';
+import { GlAvatarLabeled, GlAvatarLink } from '@gitlab/ui';
import { getByText as getByTextHelper } from '@testing-library/dom';
import { mount, createWrapper } from '@vue/test-utils';
import GroupAvatar from '~/members/components/avatars/group_avatar.vue';
-import { group as member } from '../../mock_data';
+import PrivateIcon from '~/members/components/icons/private_icon.vue';
+import { group as member, privateGroup as privateMember } from '../../mock_data';
describe('MemberList', () => {
let wrapper;
@@ -21,11 +22,9 @@ describe('MemberList', () => {
const getByText = (text, options) =>
createWrapper(getByTextHelper(wrapper.element, text, options));
- beforeEach(() => {
+ it('renders link to group', () => {
createComponent();
- });
- it('renders link to group', () => {
const link = wrapper.findComponent(GlAvatarLink);
expect(link.exists()).toBe(true);
@@ -33,10 +32,26 @@ describe('MemberList', () => {
});
it("renders group's full name", () => {
+ createComponent();
+
expect(getByText(group.fullName).exists()).toBe(true);
});
it("renders group's avatar", () => {
+ createComponent();
+
expect(wrapper.find('img').attributes('src')).toBe(group.avatarUrl);
});
+
+ describe('when group is private', () => {
+ beforeEach(() => {
+ createComponent({ member: privateMember });
+ });
+
+ it('renders private avatar with icon', () => {
+ expect(wrapper.findComponent(GlAvatarLink).exists()).toBe(false);
+ expect(wrapper.findComponent(GlAvatarLabeled).props('label')).toBe('Private');
+ expect(wrapper.findComponent(PrivateIcon).exists()).toBe(true);
+ });
+ });
});
diff --git a/spec/frontend/members/components/icons/private_icon_spec.js b/spec/frontend/members/components/icons/private_icon_spec.js
new file mode 100644
index 00000000000..ea2b65e3307
--- /dev/null
+++ b/spec/frontend/members/components/icons/private_icon_spec.js
@@ -0,0 +1,30 @@
+import { GlIcon } from '@gitlab/ui';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
+import PrivateIcon from '~/members/components/icons/private_icon.vue';
+
+describe('PrivateIcon', () => {
+ let wrapper;
+
+ const createComponent = () => {
+ wrapper = mountExtended(PrivateIcon, {
+ directives: {
+ GlTooltip: createMockDirective('gl-tooltip'),
+ },
+ });
+ };
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('renders private icon with tooltip', () => {
+ const icon = wrapper.findComponent(GlIcon);
+ const tooltipDirective = getBinding(icon.element, 'gl-tooltip');
+
+ expect(icon.props('name')).toBe('eye-slash');
+ expect(tooltipDirective.value).toBe(
+ 'Private group information is only accessible to its members.',
+ );
+ });
+});
diff --git a/spec/frontend/members/components/table/member_source_spec.js b/spec/frontend/members/components/table/member_source_spec.js
index bbfbb19fd92..16b8d239944 100644
--- a/spec/frontend/members/components/table/member_source_spec.js
+++ b/spec/frontend/members/components/table/member_source_spec.js
@@ -1,6 +1,7 @@
import { mountExtended } from 'helpers/vue_test_utils_helper';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import MemberSource from '~/members/components/table/member_source.vue';
+import PrivateIcon from '~/members/components/icons/private_icon.vue';
describe('MemberSource', () => {
let wrapper;
@@ -30,6 +31,20 @@ describe('MemberSource', () => {
const getTooltipDirective = (elementWrapper) => getBinding(elementWrapper.element, 'gl-tooltip');
+ describe('when source is private', () => {
+ beforeEach(() => {
+ createComponent({
+ isSharedWithGroupPrivate: true,
+ isDirectMember: false,
+ });
+ });
+
+ it('displays private with icon', () => {
+ expect(wrapper.findByText('Private').exists()).toBe(true);
+ expect(wrapper.findComponent(PrivateIcon).exists()).toBe(true);
+ });
+ });
+
describe('direct member', () => {
describe('when created by is available', () => {
it('displays "Direct member by <user name>"', () => {
diff --git a/spec/frontend/members/components/table/members_table_spec.js b/spec/frontend/members/components/table/members_table_spec.js
index 4539478bf9a..791155fcd1b 100644
--- a/spec/frontend/members/components/table/members_table_spec.js
+++ b/spec/frontend/members/components/table/members_table_spec.js
@@ -27,6 +27,7 @@ import {
directMember,
invite,
accessRequest,
+ privateGroup,
pagination,
} from '../../mock_data';
@@ -245,6 +246,24 @@ describe('MembersTable', () => {
});
});
});
+
+ describe('Source field', () => {
+ beforeEach(() => {
+ createComponent({
+ members: [privateGroup],
+ tableFields: ['source'],
+ });
+ });
+
+ it('passes correct props to `MemberSource` component', () => {
+ expect(wrapper.findComponent(MemberSource).props()).toMatchObject({
+ memberSource: {},
+ isDirectMember: true,
+ isSharedWithGroupPrivate: true,
+ createdBy: null,
+ });
+ });
+ });
});
describe('when `members` is an empty array', () => {
diff --git a/spec/frontend/members/components/table/role_dropdown_spec.js b/spec/frontend/members/components/table/role_dropdown_spec.js
index 5204ac2fdbe..62275a05dc5 100644
--- a/spec/frontend/members/components/table/role_dropdown_spec.js
+++ b/spec/frontend/members/components/table/role_dropdown_spec.js
@@ -1,10 +1,10 @@
import { GlCollapsibleListbox, GlListboxItem } from '@gitlab/ui';
import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils';
-import * as Sentry from '@sentry/browser';
import { mount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
// eslint-disable-next-line no-restricted-imports
import Vuex from 'vuex';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import waitForPromises from 'helpers/wait_for_promises';
import RoleDropdown from '~/members/components/table/role_dropdown.vue';
import { MEMBER_TYPES } from '~/members/constants';
@@ -13,7 +13,7 @@ import { member } from '../../mock_data';
Vue.use(Vuex);
jest.mock('ee_else_ce/members/guest_overage_confirm_action');
-jest.mock('@sentry/browser');
+jest.mock('~/sentry/sentry_browser_wrapper');
describe('RoleDropdown', () => {
let wrapper;
@@ -71,9 +71,7 @@ describe('RoleDropdown', () => {
it('has items prop with all valid roles', () => {
createComponent();
- const roles = findListbox()
- .props('items')
- .map((item) => item.text);
+ const roles = findListboxItems().wrappers.map((item) => item.text());
expect(roles).toEqual(Object.keys(member.validRoles));
});
@@ -102,7 +100,7 @@ describe('RoleDropdown', () => {
expect(actions.updateMemberRole).toHaveBeenCalledWith(expect.any(Object), {
memberId: member.id,
- accessLevel: { integerValue: 30, stringValue: 'Developer' },
+ accessLevel: { integerValue: 30, memberRoleId: null },
});
});
@@ -247,7 +245,7 @@ describe('RoleDropdown', () => {
});
it('resets selected dropdown item', () => {
- expect(findListbox().props('selected')).toBe(member.validRoles.Owner);
+ expect(findListbox().props('selected')).toMatch(/role-static-\d+/);
});
});
});
diff --git a/spec/frontend/members/mock_data.js b/spec/frontend/members/mock_data.js
index 161e96c0c48..e0dc765b9e4 100644
--- a/spec/frontend/members/mock_data.js
+++ b/spec/frontend/members/mock_data.js
@@ -41,7 +41,7 @@ export const member = {
usingLicense: false,
groupSso: false,
groupManagedAccount: false,
- provisionedByThisGroup: false,
+ enterpriseUserOfThisGroup: false,
validRoles: {
Guest: 10,
Reporter: 20,
@@ -50,6 +50,7 @@ export const member = {
Owner: 50,
'Minimal access': 5,
},
+ customRoles: [],
};
export const group = {
@@ -69,6 +70,19 @@ export const group = {
validRoles: { Guest: 10, Reporter: 20, Developer: 30, Maintainer: 40, Owner: 50 },
};
+export const privateGroup = {
+ accessLevel: { integerValue: 10, stringValue: 'Guest' },
+ isSharedWithGroupPrivate: true,
+ sharedWithGroup: {
+ id: 24,
+ },
+ id: 3,
+ isDirectMember: true,
+ createdAt: '2020-08-06T15:31:07.662Z',
+ expiresAt: null,
+ validRoles: { Guest: 10, Reporter: 20, Developer: 30, Maintainer: 40, Owner: 50 },
+};
+
export const modalData = {
isAccessRequest: true,
isInvite: true,
diff --git a/spec/frontend/members/store/actions_spec.js b/spec/frontend/members/store/actions_spec.js
index 38214048b23..3df3d85c4f1 100644
--- a/spec/frontend/members/store/actions_spec.js
+++ b/spec/frontend/members/store/actions_spec.js
@@ -15,6 +15,8 @@ import {
} from '~/members/store/actions';
import * as types from '~/members/store/mutation_types';
+const mockedRequestFormatter = jest.fn().mockImplementation(noop);
+
describe('Vuex members actions', () => {
describe('update member actions', () => {
let mock;
@@ -22,7 +24,7 @@ describe('Vuex members actions', () => {
const state = {
members,
memberPath: '/groups/foo-bar/-/group_members/:id',
- requestFormatter: noop,
+ requestFormatter: mockedRequestFormatter,
};
beforeEach(() => {
@@ -35,7 +37,7 @@ describe('Vuex members actions', () => {
describe('updateMemberRole', () => {
const memberId = members[0].id;
- const accessLevel = { integerValue: 30, stringValue: 'Developer' };
+ const accessLevel = { integerValue: 30, memberRoleId: 90 };
const payload = {
memberId,
@@ -54,6 +56,10 @@ describe('Vuex members actions', () => {
]);
expect(mock.history.put[0].url).toBe('/groups/foo-bar/-/group_members/238');
+ expect(mockedRequestFormatter).toHaveBeenCalledWith({
+ accessLevel: accessLevel.integerValue,
+ memberRoleId: accessLevel.memberRoleId,
+ });
});
});
diff --git a/spec/frontend/members/utils_spec.js b/spec/frontend/members/utils_spec.js
index c4357e9c1f0..54f5433c9c9 100644
--- a/spec/frontend/members/utils_spec.js
+++ b/spec/frontend/members/utils_spec.js
@@ -22,6 +22,8 @@ import {
buildSortHref,
parseDataAttributes,
groupLinkRequestFormatter,
+ roleDropdownItems,
+ initialSelectedRole,
} from '~/members/utils';
import {
member as memberMock,
@@ -35,6 +37,8 @@ import {
dataAttribute,
} from './mock_data';
+jest.mock('lodash/uniqueId', () => (prefix) => `${prefix}0`);
+
const IS_CURRENT_USER_ID = 123;
const IS_NOT_CURRENT_USER_ID = 124;
const URL_HOST = 'https://localhost/';
@@ -317,7 +321,46 @@ describe('Members Utils', () => {
accessLevel: 50,
expires_at: '2020-10-16',
}),
- ).toEqual({ group_link: { group_access: 50, expires_at: '2020-10-16' } });
+ ).toEqual({
+ group_link: { group_access: 50, expires_at: '2020-10-16', member_role_id: null },
+ });
+
+ expect(
+ groupLinkRequestFormatter({
+ accessLevel: 50,
+ expires_at: '2020-10-16',
+ memberRoleId: 80,
+ }),
+ ).toEqual({
+ group_link: { group_access: 50, expires_at: '2020-10-16', member_role_id: 80 },
+ });
+ });
+ });
+
+ describe('roleDropdownItems', () => {
+ it('returns properly flatten and formatted dropdowns', () => {
+ const { flatten, formatted } = roleDropdownItems(members[0]);
+
+ expect(flatten).toEqual(formatted);
+ expect(flatten[0]).toMatchObject({
+ text: 'Guest',
+ value: 'role-static-0',
+ accessLevel: 10,
+ memberRoleId: null,
+ });
+ });
+ });
+
+ describe('initialSelectedRole', () => {
+ it('find and return correct value', () => {
+ expect(
+ initialSelectedRole(
+ [{ accessLevel: 10, memberRoleId: null, text: 'Guest', value: 'role-static-0' }],
+ {
+ accessLevel: { integerValue: 10 },
+ },
+ ),
+ ).toBe('role-static-0');
});
});
});
diff --git a/spec/frontend/merge_requests/components/sticky_header_spec.js b/spec/frontend/merge_requests/components/sticky_header_spec.js
new file mode 100644
index 00000000000..9fc265cd9ad
--- /dev/null
+++ b/spec/frontend/merge_requests/components/sticky_header_spec.js
@@ -0,0 +1,48 @@
+import Vue from 'vue';
+// eslint-disable-next-line no-restricted-imports
+import Vuex from 'vuex';
+import { GlSprintf } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import StickyHeader from '~/merge_requests/components/sticky_header.vue';
+
+Vue.use(Vuex);
+
+let wrapper;
+
+function createComponent(provide = {}) {
+ const store = new Vuex.Store({
+ state: {
+ page: { activeTab: 'overview' },
+ notes: { notes: { doneFetchingBatchDiscussions: true } },
+ },
+ getters: {
+ getNoteableData: () => ({
+ id: 1,
+ source_branch: 'source-branch',
+ target_branch: 'main',
+ }),
+ discussionTabCounter: () => 1,
+ },
+ });
+
+ wrapper = shallowMountExtended(StickyHeader, {
+ store,
+ provide,
+ stubs: {
+ GlSprintf,
+ },
+ });
+}
+
+describe('Merge requests sticky header component', () => {
+ describe('forked project', () => {
+ it('renders source branch with source project path', () => {
+ createComponent({
+ projectPath: 'gitlab-org/gitlab',
+ sourceProjectPath: 'root/gitlab',
+ });
+
+ expect(wrapper.findByTestId('source-branch').text()).toBe('root/gitlab:source-branch');
+ });
+ });
+});
diff --git a/spec/frontend/milestones/components/milestone_combobox_spec.js b/spec/frontend/milestones/components/milestone_combobox_spec.js
index 53abf6dc544..3e2cd354d53 100644
--- a/spec/frontend/milestones/components/milestone_combobox_spec.js
+++ b/spec/frontend/milestones/components/milestone_combobox_spec.js
@@ -1,12 +1,11 @@
-import { GlLoadingIcon, GlSearchBoxByType, GlDropdownItem } from '@gitlab/ui';
+import { GlLoadingIcon, GlCollapsibleListbox, GlListboxItem } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
-import Vue, { nextTick } from 'vue';
+import Vue from 'vue';
// eslint-disable-next-line no-restricted-imports
import Vuex from 'vuex';
import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
-import { ENTER_KEY } from '~/lib/utils/keys';
import MilestoneCombobox from '~/milestones/components/milestone_combobox.vue';
import createStore from '~/milestones/stores/';
import { projectMilestones, groupMilestones } from '../mock_data';
@@ -52,9 +51,6 @@ describe('Milestone combobox component', () => {
wrapper.setProps({ value: selectedMilestone });
},
},
- stubs: {
- GlSearchBoxByType: true,
- },
store: createStore(),
});
};
@@ -89,57 +85,25 @@ describe('Milestone combobox component', () => {
//
// Finders
//
- const findButtonContent = () => wrapper.find('[data-testid="milestone-combobox-button-content"]');
-
- const findNoResults = () => wrapper.find('[data-testid="milestone-combobox-no-results"]');
-
+ const findGlCollapsibleListbox = () => wrapper.findComponent(GlCollapsibleListbox);
+ const findButtonContent = () => wrapper.find('[data-testid="base-dropdown-toggle"]');
+ const findNoResults = () => wrapper.find('[data-testid="listbox-no-results-text"]');
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
-
- const findSearchBox = () => wrapper.findComponent(GlSearchBoxByType);
-
const findProjectMilestonesSection = () =>
- wrapper.find('[data-testid="project-milestones-section"]');
- const findProjectMilestonesDropdownItems = () =>
- findProjectMilestonesSection().findAllComponents(GlDropdownItem);
- const findFirstProjectMilestonesDropdownItem = () => findProjectMilestonesDropdownItems().at(0);
-
- const findGroupMilestonesSection = () => wrapper.find('[data-testid="group-milestones-section"]');
- const findGroupMilestonesDropdownItems = () =>
- findGroupMilestonesSection().findAllComponents(GlDropdownItem);
- const findFirstGroupMilestonesDropdownItem = () => findGroupMilestonesDropdownItems().at(0);
-
- //
- // Expecters
- //
- const projectMilestoneSectionContainsErrorMessage = () => {
- const projectMilestoneSection = findProjectMilestonesSection();
-
- return projectMilestoneSection
- .text()
- .includes('An error occurred while searching for milestones');
- };
-
- const groupMilestoneSectionContainsErrorMessage = () => {
- const groupMilestoneSection = findGroupMilestonesSection();
-
- return groupMilestoneSection
- .text()
- .includes('An error occurred while searching for milestones');
- };
+ findGlCollapsibleListbox().find('[data-testid="project-milestones-section"]');
+ const findGroupMilestonesSection = () =>
+ findGlCollapsibleListbox().find('[data-testid="group-milestones-section"]');
+ const findDropdownItems = () => findGlCollapsibleListbox().findAllComponents(GlListboxItem);
//
// Convenience methods
//
const updateQuery = (newQuery) => {
- findSearchBox().vm.$emit('input', newQuery);
+ findGlCollapsibleListbox().vm.$emit('search', newQuery);
};
- const selectFirstProjectMilestone = () => {
- findFirstProjectMilestonesDropdownItem().vm.$emit('click');
- };
-
- const selectFirstGroupMilestone = () => {
- findFirstGroupMilestonesDropdownItem().vm.$emit('click');
+ const selectItem = (item) => {
+ findGlCollapsibleListbox().vm.$emit('select', item);
};
const waitForRequests = async ({ andClearMocks } = { andClearMocks: false }) => {
@@ -224,22 +188,6 @@ describe('Milestone combobox component', () => {
});
});
- describe('when the Enter is pressed', () => {
- beforeEach(() => {
- createComponent();
-
- return waitForRequests({ andClearMocks: true });
- });
-
- it('requeries the search when Enter is pressed', () => {
- findSearchBox().vm.$emit('keydown', new KeyboardEvent({ key: ENTER_KEY }));
-
- return waitForRequests().then(() => {
- expect(searchApiCallSpy).toHaveBeenCalledTimes(1);
- });
- });
- });
-
describe('when no results are found', () => {
beforeEach(() => {
projectMilestonesApiCallSpy = jest
@@ -257,7 +205,7 @@ describe('Milestone combobox component', () => {
describe('when the search query is empty', () => {
it('renders a "no results" message', () => {
- expect(findNoResults().text()).toBe('No matching results');
+ expect(findNoResults().text()).toBe('No results found');
});
});
});
@@ -275,22 +223,14 @@ describe('Milestone combobox component', () => {
});
it('renders the "Project milestones" heading with a total number indicator', () => {
- expect(
- findProjectMilestonesSection()
- .find('[data-testid="milestone-results-section-header"]')
- .text(),
- ).toBe('Project milestones 6');
- });
-
- it("does not render an error message in the project milestone section's body", () => {
- expect(projectMilestoneSectionContainsErrorMessage()).toBe(false);
+ expect(findProjectMilestonesSection().text()).toBe('Project milestones 6');
});
it('renders each project milestones as a selectable item', () => {
- const dropdownItems = findProjectMilestonesDropdownItems();
+ const dropdownItems = findDropdownItems();
- projectMilestones.forEach((milestone, i) => {
- expect(dropdownItems.at(i).text()).toBe(milestone.title);
+ projectMilestones.forEach((milestone) => {
+ expect(dropdownItems.filter((x) => x.text() === milestone.title).exists()).toBe(true);
});
});
});
@@ -323,12 +263,8 @@ describe('Milestone combobox component', () => {
return waitForRequests();
});
- it('renders the project milestones section in the dropdown', () => {
- expect(findProjectMilestonesSection().exists()).toBe(true);
- });
-
- it("renders an error message in the project milestones section's body", () => {
- expect(projectMilestoneSectionContainsErrorMessage()).toBe(true);
+ it('does not render the project milestones section in the dropdown', () => {
+ expect(findProjectMilestonesSection().exists()).toBe(false);
});
});
@@ -339,52 +275,24 @@ describe('Milestone combobox component', () => {
return waitForRequests();
});
- it('renders a checkmark by the selected item', async () => {
- selectFirstProjectMilestone();
-
- await nextTick();
-
- expect(
- findFirstProjectMilestonesDropdownItem()
- .find('svg')
- .classes('gl-dropdown-item-check-icon'),
- ).toBe(true);
-
- selectFirstProjectMilestone();
-
- await nextTick();
-
- expect(
- findFirstProjectMilestonesDropdownItem().find('svg').classes('gl-visibility-hidden'),
- ).toBe(true);
- });
+ describe('when a project milestone is selected', () => {
+ const item = 'v1.0';
- describe('when a project milestones is selected', () => {
beforeEach(() => {
createComponent();
projectMilestonesApiCallSpy = jest
.fn()
.mockReturnValue([HTTP_STATUS_OK, [{ title: 'v1.0' }], { [X_TOTAL_HEADER]: '1' }]);
+ selectItem([item]);
return waitForRequests();
});
- it("displays the project milestones name in the dropdown's button", async () => {
- selectFirstProjectMilestone();
- await nextTick();
-
- expect(findButtonContent().text()).toBe('v1.0');
-
- selectFirstProjectMilestone();
- await nextTick();
-
- expect(findButtonContent().text()).toBe('No milestone');
+ it("displays the project milestones name in the dropdown's button", () => {
+ expect(findButtonContent().text()).toBe(item);
});
- it('updates the v-model binding with the project milestone title', async () => {
- selectFirstProjectMilestone();
- await nextTick();
-
+ it('updates the v-model binding with the project milestone title', () => {
expect(wrapper.emitted().input[0][0]).toStrictEqual(['v1.0']);
});
});
@@ -404,22 +312,14 @@ describe('Milestone combobox component', () => {
});
it('renders the "Group milestones" heading with a total number indicator', () => {
- expect(
- findGroupMilestonesSection()
- .find('[data-testid="milestone-results-section-header"]')
- .text(),
- ).toBe('Group milestones 6');
- });
-
- it("does not render an error message in the group milestone section's body", () => {
- expect(groupMilestoneSectionContainsErrorMessage()).toBe(false);
+ expect(findGroupMilestonesSection().text()).toBe('Group milestones 6');
});
it('renders each group milestones as a selectable item', () => {
- const dropdownItems = findGroupMilestonesDropdownItems();
+ const dropdownItems = findDropdownItems();
- groupMilestones.forEach((milestone, i) => {
- expect(dropdownItems.at(i).text()).toBe(milestone.title);
+ groupMilestones.forEach((milestone) => {
+ expect(dropdownItems.filter((x) => x.text() === milestone.title).exists()).toBe(true);
});
});
});
@@ -452,74 +352,8 @@ describe('Milestone combobox component', () => {
return waitForRequests();
});
- it('renders the group milestones section in the dropdown', () => {
- expect(findGroupMilestonesSection().exists()).toBe(true);
- });
-
- it("renders an error message in the group milestones section's body", () => {
- expect(groupMilestoneSectionContainsErrorMessage()).toBe(true);
- });
- });
-
- describe('selection', () => {
- beforeEach(() => {
- createComponent();
-
- return waitForRequests();
- });
-
- it('renders a checkmark by the selected item', async () => {
- selectFirstGroupMilestone();
-
- await nextTick();
-
- expect(
- findFirstGroupMilestonesDropdownItem()
- .find('svg')
- .classes('gl-dropdown-item-check-icon'),
- ).toBe(true);
-
- selectFirstGroupMilestone();
-
- await nextTick();
-
- expect(
- findFirstGroupMilestonesDropdownItem().find('svg').classes('gl-visibility-hidden'),
- ).toBe(true);
- });
-
- describe('when a group milestones is selected', () => {
- beforeEach(() => {
- createComponent();
- groupMilestonesApiCallSpy = jest
- .fn()
- .mockReturnValue([
- HTTP_STATUS_OK,
- [{ title: 'group-v1.0' }],
- { [X_TOTAL_HEADER]: '1' },
- ]);
-
- return waitForRequests();
- });
-
- it("displays the group milestones name in the dropdown's button", async () => {
- selectFirstGroupMilestone();
- await nextTick();
-
- expect(findButtonContent().text()).toBe('group-v1.0');
-
- selectFirstGroupMilestone();
- await nextTick();
-
- expect(findButtonContent().text()).toBe('No milestone');
- });
-
- it('updates the v-model binding with the group milestone title', async () => {
- selectFirstGroupMilestone();
- await nextTick();
-
- expect(wrapper.emitted().input[0][0]).toStrictEqual(['group-v1.0']);
- });
+ it('does not render the group milestones section', () => {
+ expect(findGroupMilestonesSection().exists()).toBe(false);
});
});
});
diff --git a/spec/frontend/milestones/stores/mutations_spec.js b/spec/frontend/milestones/stores/mutations_spec.js
index a53d6ca5de1..e3a5e4d00f4 100644
--- a/spec/frontend/milestones/stores/mutations_spec.js
+++ b/spec/frontend/milestones/stores/mutations_spec.js
@@ -163,10 +163,12 @@ describe('Milestones combobox Vuex store mutations', () => {
expect(state.matches.projectMilestones).toEqual({
list: [
{
- title: 'v0.1',
+ text: 'v0.1',
+ value: 'v0.1',
},
{
- title: 'v0.2',
+ text: 'v0.2',
+ value: 'v0.2',
},
],
error: null,
@@ -192,10 +194,12 @@ describe('Milestones combobox Vuex store mutations', () => {
expect(state.matches.projectMilestones).toEqual({
list: [
{
- title: 'v0.1',
+ text: 'v0.1',
+ value: 'v0.1',
},
{
- title: 'v0.2',
+ text: 'v0.2',
+ value: 'v0.2',
},
],
error: null,
@@ -245,10 +249,12 @@ describe('Milestones combobox Vuex store mutations', () => {
expect(state.matches.groupMilestones).toEqual({
list: [
{
- title: 'group-0.1',
+ text: 'group-0.1',
+ value: 'group-0.1',
},
{
- title: 'group-0.2',
+ text: 'group-0.2',
+ value: 'group-0.2',
},
],
error: null,
@@ -274,10 +280,12 @@ describe('Milestones combobox Vuex store mutations', () => {
expect(state.matches.groupMilestones).toEqual({
list: [
{
- title: 'group-0.1',
+ text: 'group-0.1',
+ value: 'group-0.1',
},
{
- title: 'group-0.2',
+ text: 'group-0.2',
+ value: 'group-0.2',
},
],
error: null,
diff --git a/spec/frontend/ml/model_registry/routes/models/index/components/ml_models_index_spec.js b/spec/frontend/ml/model_registry/apps/index_ml_models_spec.js
index c1b9aef9634..6e0ab2ebe2d 100644
--- a/spec/frontend/ml/model_registry/routes/models/index/components/ml_models_index_spec.js
+++ b/spec/frontend/ml/model_registry/apps/index_ml_models_spec.js
@@ -1,19 +1,27 @@
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import MlModelsIndexApp from '~/ml/model_registry/routes/models/index';
-import ModelRow from '~/ml/model_registry/routes/models/index/components/model_row.vue';
-import { TITLE_LABEL, NO_MODELS_LABEL } from '~/ml/model_registry/routes/models/index/translations';
+import { IndexMlModels } from '~/ml/model_registry/apps';
+import ModelRow from '~/ml/model_registry/components/model_row.vue';
+import { TITLE_LABEL, NO_MODELS_LABEL } from '~/ml/model_registry/translations';
import Pagination from '~/vue_shared/components/incubation/pagination.vue';
-import { mockModels, startCursor, defaultPageInfo } from './mock_data';
+import SearchBar from '~/ml/model_registry/components/search_bar.vue';
+import { BASE_SORT_FIELDS } from '~/ml/model_registry/constants';
+import TitleArea from '~/vue_shared/components/registry/title_area.vue';
+import MetadataItem from '~/vue_shared/components/registry/metadata_item.vue';
+import { mockModels, startCursor, defaultPageInfo } from '../mock_data';
let wrapper;
-const createWrapper = (propsData = { models: mockModels, pageInfo: defaultPageInfo }) => {
- wrapper = shallowMountExtended(MlModelsIndexApp, { propsData });
+const createWrapper = (
+ propsData = { models: mockModels, pageInfo: defaultPageInfo, modelCount: 2 },
+) => {
+ wrapper = shallowMountExtended(IndexMlModels, { propsData });
};
const findModelRow = (index) => wrapper.findAllComponents(ModelRow).at(index);
const findPagination = () => wrapper.findComponent(Pagination);
-const findTitle = () => wrapper.findByText(TITLE_LABEL);
const findEmptyLabel = () => wrapper.findByText(NO_MODELS_LABEL);
+const findSearchBar = () => wrapper.findComponent(SearchBar);
+const findTitleArea = () => wrapper.findComponent(TitleArea);
+const findModelCountMetadataItem = () => findTitleArea().findComponent(MetadataItem);
describe('MlModelsIndex', () => {
describe('empty state', () => {
@@ -26,6 +34,10 @@ describe('MlModelsIndex', () => {
it('does not show pagination', () => {
expect(findPagination().exists()).toBe(false);
});
+
+ it('does not show search bar', () => {
+ expect(findSearchBar().exists()).toBe(false);
+ });
});
describe('with data', () => {
@@ -39,10 +51,18 @@ describe('MlModelsIndex', () => {
describe('header', () => {
it('displays the title', () => {
- expect(findTitle().exists()).toBe(true);
+ expect(findTitleArea().props('title')).toBe(TITLE_LABEL);
+ });
+
+ it('sets model metadata item to model count', () => {
+ expect(findModelCountMetadataItem().props('text')).toBe(`2 models`);
});
});
+ it('adds a search bar', () => {
+ expect(findSearchBar().props()).toMatchObject({ sortableFields: BASE_SORT_FIELDS });
+ });
+
describe('model list', () => {
it('displays the models', () => {
expect(findModelRow(0).props('model')).toMatchObject(mockModels[0]);
diff --git a/spec/frontend/ml/model_registry/apps/show_ml_model_spec.js b/spec/frontend/ml/model_registry/apps/show_ml_model_spec.js
index 57a5a5f003f..bc4770976a9 100644
--- a/spec/frontend/ml/model_registry/apps/show_ml_model_spec.js
+++ b/spec/frontend/ml/model_registry/apps/show_ml_model_spec.js
@@ -1,15 +1,78 @@
+import { GlBadge, GlTab } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { ShowMlModel } from '~/ml/model_registry/apps';
-import { MODEL } from '../mock_data';
+import TitleArea from '~/vue_shared/components/registry/title_area.vue';
+import MetadataItem from '~/vue_shared/components/registry/metadata_item.vue';
+import { NO_VERSIONS_LABEL } from '~/ml/model_registry/translations';
+import { MODEL, makeModel } from '../mock_data';
let wrapper;
-const createWrapper = () => {
- wrapper = shallowMount(ShowMlModel, { propsData: { model: MODEL } });
+const createWrapper = (model = MODEL) => {
+ wrapper = shallowMount(ShowMlModel, { propsData: { model } });
};
+const findDetailTab = () => wrapper.findAllComponents(GlTab).at(0);
+const findVersionsTab = () => wrapper.findAllComponents(GlTab).at(1);
+const findVersionsCountBadge = () => findVersionsTab().findComponent(GlBadge);
+const findCandidateTab = () => wrapper.findAllComponents(GlTab).at(2);
+const findCandidatesCountBadge = () => findCandidateTab().findComponent(GlBadge);
+const findTitleArea = () => wrapper.findComponent(TitleArea);
+const findVersionCountMetadataItem = () => findTitleArea().findComponent(MetadataItem);
+
describe('ShowMlModel', () => {
- beforeEach(() => createWrapper());
- it('renders the app', () => {
- expect(wrapper.text()).toContain(MODEL.name);
+ describe('Title', () => {
+ beforeEach(() => createWrapper());
+
+ it('title is set to model name', () => {
+ expect(findTitleArea().props('title')).toBe(MODEL.name);
+ });
+
+ it('subheader is set to description', () => {
+ expect(findTitleArea().text()).toContain(MODEL.description);
+ });
+
+ it('sets version metadata item to version count', () => {
+ expect(findVersionCountMetadataItem().props('text')).toBe(`${MODEL.versionCount} versions`);
+ });
+ });
+
+ describe('Details', () => {
+ beforeEach(() => createWrapper());
+
+ it('has a details tab', () => {
+ expect(findDetailTab().attributes('title')).toBe('Details');
+ });
+
+ describe('when it has latest version', () => {
+ it('displays the version', () => {
+ expect(findDetailTab().text()).toContain(MODEL.latestVersion.version);
+ });
+ });
+
+ describe('when it does not have latest version', () => {
+ beforeEach(() => {
+ createWrapper(makeModel({ latestVersion: null }));
+ });
+
+ it('shows no version message', () => {
+ expect(findDetailTab().text()).toContain(NO_VERSIONS_LABEL);
+ });
+ });
+ });
+
+ describe('Versions tab', () => {
+ beforeEach(() => createWrapper());
+
+ it('shows the number of versions in the tab', () => {
+ expect(findVersionsCountBadge().text()).toBe(MODEL.versionCount.toString());
+ });
+ });
+
+ describe('Candidates tab', () => {
+ beforeEach(() => createWrapper());
+
+ it('shows the number of candidates in the tab', () => {
+ expect(findCandidatesCountBadge().text()).toBe(MODEL.candidateCount.toString());
+ });
});
});
diff --git a/spec/frontend/ml/model_registry/apps/show_ml_model_version_spec.js b/spec/frontend/ml/model_registry/apps/show_ml_model_version_spec.js
new file mode 100644
index 00000000000..77fca53c00e
--- /dev/null
+++ b/spec/frontend/ml/model_registry/apps/show_ml_model_version_spec.js
@@ -0,0 +1,15 @@
+import { shallowMount } from '@vue/test-utils';
+import { ShowMlModelVersion } from '~/ml/model_registry/apps';
+import { MODEL_VERSION } from '../mock_data';
+
+let wrapper;
+const createWrapper = () => {
+ wrapper = shallowMount(ShowMlModelVersion, { propsData: { modelVersion: MODEL_VERSION } });
+};
+
+describe('ShowMlModelVersion', () => {
+ beforeEach(() => createWrapper());
+ it('renders the app', () => {
+ expect(wrapper.text()).toContain(`${MODEL_VERSION.model.name} - ${MODEL_VERSION.version}`);
+ });
+});
diff --git a/spec/frontend/ml/model_registry/components/model_row_spec.js b/spec/frontend/ml/model_registry/components/model_row_spec.js
new file mode 100644
index 00000000000..9d16ce5c466
--- /dev/null
+++ b/spec/frontend/ml/model_registry/components/model_row_spec.js
@@ -0,0 +1,45 @@
+import { GlLink } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import ModelRow from '~/ml/model_registry/components/model_row.vue';
+import { mockModels, modelWithoutVersion } from '../mock_data';
+
+let wrapper;
+const createWrapper = (model = mockModels[0]) => {
+ wrapper = shallowMountExtended(ModelRow, { propsData: { model } });
+};
+
+const findTitleLink = () => wrapper.findAllComponents(GlLink).at(0);
+const findVersionLink = () => wrapper.findAllComponents(GlLink).at(1);
+const findMessage = (message) => wrapper.findByText(message);
+
+describe('ModelRow', () => {
+ it('Has a link to the model', () => {
+ createWrapper();
+
+ expect(findTitleLink().text()).toBe(mockModels[0].name);
+ expect(findTitleLink().attributes('href')).toBe(mockModels[0].path);
+ });
+
+ it('Shows the latest version and the version count', () => {
+ createWrapper();
+
+ expect(findVersionLink().text()).toBe(mockModels[0].version);
+ expect(findVersionLink().attributes('href')).toBe(mockModels[0].versionPath);
+ expect(findMessage('· 3 versions').exists()).toBe(true);
+ });
+
+ it('Shows the latest version and no version count if it has only 1 version', () => {
+ createWrapper(mockModels[1]);
+
+ expect(findVersionLink().text()).toBe(mockModels[1].version);
+ expect(findVersionLink().attributes('href')).toBe(mockModels[1].versionPath);
+
+ expect(findMessage('· No other versions').exists()).toBe(true);
+ });
+
+ it('Shows no version message if model has no versions', () => {
+ createWrapper(modelWithoutVersion);
+
+ expect(findMessage('No registered versions').exists()).toBe(true);
+ });
+});
diff --git a/spec/frontend/ml/model_registry/components/search_bar_spec.js b/spec/frontend/ml/model_registry/components/search_bar_spec.js
new file mode 100644
index 00000000000..f9e18486434
--- /dev/null
+++ b/spec/frontend/ml/model_registry/components/search_bar_spec.js
@@ -0,0 +1,86 @@
+import { shallowMount } from '@vue/test-utils';
+import setWindowLocation from 'helpers/set_window_location_helper';
+import * as urlHelpers from '~/lib/utils/url_utility';
+import SearchBar from '~/ml/model_registry/components/search_bar.vue';
+import { BASE_SORT_FIELDS } from '~/ml/model_registry/constants';
+import RegistrySearch from '~/vue_shared/components/registry/registry_search.vue';
+
+let wrapper;
+
+const makeUrl = ({ filter = 'query', orderBy = 'name', sort = 'asc' } = {}) =>
+ `https://blah.com/?name=${filter}&orderBy=${orderBy}&sort=${sort}`;
+
+const createWrapper = () => {
+ wrapper = shallowMount(SearchBar, { propsData: { sortableFields: BASE_SORT_FIELDS } });
+};
+
+const findRegistrySearch = () => wrapper.findComponent(RegistrySearch);
+
+describe('SearchBar', () => {
+ beforeEach(() => {
+ createWrapper();
+ });
+
+ it('passes default filter and sort by to registry search', () => {
+ expect(findRegistrySearch().props()).toMatchObject({
+ filters: [],
+ sorting: {
+ orderBy: 'created_at',
+ sort: 'desc',
+ },
+ sortableFields: BASE_SORT_FIELDS,
+ });
+ });
+
+ it('sets the component filters based on the querystring', () => {
+ const filter = 'A';
+ setWindowLocation(makeUrl({ filter }));
+
+ createWrapper();
+
+ expect(findRegistrySearch().props('filters')).toMatchObject([{ value: { data: filter } }]);
+ });
+
+ it('sets the registry search sort based on the querystring', () => {
+ const orderBy = 'B';
+ const sort = 'C';
+
+ setWindowLocation(makeUrl({ orderBy, sort }));
+
+ createWrapper();
+
+ expect(findRegistrySearch().props('sorting')).toMatchObject({ orderBy, sort: 'c' });
+ });
+
+ describe('Search submit', () => {
+ beforeEach(() => {
+ setWindowLocation(makeUrl());
+ jest.spyOn(urlHelpers, 'visitUrl').mockImplementation(() => {});
+
+ createWrapper();
+ });
+
+ it('On submit, resets the cursor and reloads to correct page', () => {
+ findRegistrySearch().vm.$emit('filter:submit');
+
+ expect(urlHelpers.visitUrl).toHaveBeenCalledTimes(1);
+ expect(urlHelpers.visitUrl).toHaveBeenCalledWith(makeUrl());
+ });
+
+ it('On sorting changed, resets cursor and reloads to correct page', () => {
+ const orderBy = 'created_at';
+ findRegistrySearch().vm.$emit('sorting:changed', { orderBy });
+
+ expect(urlHelpers.visitUrl).toHaveBeenCalledTimes(1);
+ expect(urlHelpers.visitUrl).toHaveBeenCalledWith(makeUrl({ orderBy }));
+ });
+
+ it('On direction changed, reloads to correct page', () => {
+ const sort = 'asc';
+ findRegistrySearch().vm.$emit('sorting:changed', { sort });
+
+ expect(urlHelpers.visitUrl).toHaveBeenCalledTimes(1);
+ expect(urlHelpers.visitUrl).toHaveBeenCalledWith(makeUrl({ sort }));
+ });
+ });
+});
diff --git a/spec/frontend/ml/model_registry/mock_data.js b/spec/frontend/ml/model_registry/mock_data.js
index 18b2b32e069..a820c323103 100644
--- a/spec/frontend/ml/model_registry/mock_data.js
+++ b/spec/frontend/ml/model_registry/mock_data.js
@@ -1 +1,48 @@
-export const MODEL = { name: 'blah' };
+const LATEST_VERSION = {
+ version: '1.2.3',
+};
+
+export const makeModel = ({ latestVersion } = { latestVersion: LATEST_VERSION }) => ({
+ id: 1234,
+ name: 'blah',
+ path: 'path/to/blah',
+ description: 'Description of the model',
+ latestVersion,
+ versionCount: 2,
+ candidateCount: 1,
+});
+
+export const MODEL = makeModel();
+
+export const MODEL_VERSION = { version: '1.2.3', model: MODEL };
+
+export const mockModels = [
+ {
+ name: 'model_1',
+ version: '1.0',
+ versionPath: 'path/to/version',
+ path: 'path/to/model_1',
+ versionCount: 3,
+ },
+ {
+ name: 'model_2',
+ version: '1.1',
+ path: 'path/to/model_2',
+ versionCount: 1,
+ },
+];
+
+export const modelWithoutVersion = {
+ name: 'model_without_version',
+ path: 'path/to/model_without_version',
+ versionCount: 0,
+};
+
+export const startCursor = 'eyJpZCI6IjE2In0';
+
+export const defaultPageInfo = Object.freeze({
+ startCursor,
+ endCursor: 'eyJpZCI6IjIifQ',
+ hasNextPage: true,
+ hasPreviousPage: true,
+});
diff --git a/spec/frontend/ml/model_registry/routes/models/index/components/model_row_spec.js b/spec/frontend/ml/model_registry/routes/models/index/components/model_row_spec.js
deleted file mode 100644
index 7600288f560..00000000000
--- a/spec/frontend/ml/model_registry/routes/models/index/components/model_row_spec.js
+++ /dev/null
@@ -1,42 +0,0 @@
-import { GlLink } from '@gitlab/ui';
-import {
- mockModels,
- modelWithoutVersion,
-} from 'jest/ml/model_registry/routes/models/index/components/mock_data';
-import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import ModelRow from '~/ml/model_registry/routes/models/index/components/model_row.vue';
-
-let wrapper;
-const createWrapper = (model = mockModels[0]) => {
- wrapper = shallowMountExtended(ModelRow, { propsData: { model } });
-};
-
-const findLink = () => wrapper.findComponent(GlLink);
-const findMessage = (message) => wrapper.findByText(message);
-
-describe('ModelRow', () => {
- beforeEach(() => {
- createWrapper();
- });
-
- it('Has a link to the model', () => {
- expect(findLink().text()).toBe(mockModels[0].name);
- expect(findLink().attributes('href')).toBe(mockModels[0].path);
- });
-
- it('Shows the latest version and the version count', () => {
- expect(findMessage('1.0 · 3 versions').exists()).toBe(true);
- });
-
- it('Shows the latest version and no version count if it has only 1 version', () => {
- createWrapper(mockModels[1]);
-
- expect(findMessage('1.1 · No other versions').exists()).toBe(true);
- });
-
- it('Shows no version message if model has no versions', () => {
- createWrapper(modelWithoutVersion);
-
- expect(findMessage('No registered versions').exists()).toBe(true);
- });
-});
diff --git a/spec/frontend/notes/components/comment_field_layout_spec.js b/spec/frontend/notes/components/comment_field_layout_spec.js
index 93b54f95021..b55019ed525 100644
--- a/spec/frontend/notes/components/comment_field_layout_spec.js
+++ b/spec/frontend/notes/components/comment_field_layout_spec.js
@@ -31,19 +31,13 @@ describe('Comment Field Layout Component', () => {
const findAttachmentsWarning = () => wrapper.findComponent(AttachmentsWarning);
const findErrorAlert = () => wrapper.findByTestId('comment-field-alert-container');
- const createWrapper = (props = {}, provide = {}) => {
+ const createWrapper = (props = {}) => {
wrapper = extendedWrapper(
shallowMount(CommentFieldLayout, {
propsData: {
noteableData: noteableDataMock,
...props,
},
- provide: {
- glFeatures: {
- serviceDeskNewNoteEmailNativeAttachments: true,
- },
- ...provide,
- },
}),
);
};
@@ -160,22 +154,4 @@ describe('Comment Field Layout Component', () => {
expect(findEmailParticipantsWarning().exists()).toBe(false);
});
});
-
- describe('serviceDeskNewNoteEmailNativeAttachments flag', () => {
- it('shows warning message when flag is enabled', () => {
- createWrapper(commentFieldWithAttachmentData, {
- glFeatures: { serviceDeskNewNoteEmailNativeAttachments: true },
- });
-
- expect(findAttachmentsWarning().exists()).toBe(true);
- });
-
- it('shows warning message when flag is disables', () => {
- createWrapper(commentFieldWithAttachmentData, {
- glFeatures: { serviceDeskNewNoteEmailNativeAttachments: false },
- });
-
- expect(findAttachmentsWarning().exists()).toBe(false);
- });
- });
});
diff --git a/spec/frontend/observability/client_spec.js b/spec/frontend/observability/client_spec.js
index 68a53131539..b41b303f57d 100644
--- a/spec/frontend/observability/client_spec.js
+++ b/spec/frontend/observability/client_spec.js
@@ -1,10 +1,13 @@
import MockAdapter from 'axios-mock-adapter';
-import * as Sentry from '@sentry/browser';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import { buildClient } from '~/observability/client';
import axios from '~/lib/utils/axios_utils';
+import { logError } from '~/lib/logger';
+import { DEFAULT_SORTING_OPTION, SORTING_OPTIONS } from '~/observability/constants';
jest.mock('~/lib/utils/axios_utils');
-jest.mock('@sentry/browser');
+jest.mock('~/sentry/sentry_browser_wrapper');
+jest.mock('~/lib/logger');
describe('buildClient', () => {
let client;
@@ -14,52 +17,58 @@ describe('buildClient', () => {
const provisioningUrl = 'https://example.com/provisioning';
const servicesUrl = 'https://example.com/services';
const operationsUrl = 'https://example.com/services/$SERVICE_NAME$/operations';
+ const metricsUrl = 'https://example.com/metrics';
const FETCHING_TRACES_ERROR = 'traces are missing/invalid in the response';
+ const apiConfig = {
+ tracingUrl,
+ provisioningUrl,
+ servicesUrl,
+ operationsUrl,
+ metricsUrl,
+ };
+
+ const getQueryParam = () => decodeURIComponent(axios.get.mock.calls[0][1].params.toString());
+
beforeEach(() => {
axiosMock = new MockAdapter(axios);
jest.spyOn(axios, 'get');
- client = buildClient({
- tracingUrl,
- provisioningUrl,
- servicesUrl,
- operationsUrl,
- });
+ client = buildClient(apiConfig);
});
afterEach(() => {
axiosMock.restore();
});
+ const expectErrorToBeReported = (e) => {
+ expect(Sentry.captureException).toHaveBeenCalledWith(e);
+ expect(logError).toHaveBeenCalledWith(e);
+ };
+
describe('buildClient', () => {
- it('rejects if params are missing', () => {
- const e = new Error(
- 'missing required params. provisioningUrl, tracingUrl, servicesUrl, operationsUrl are required',
- );
- expect(() =>
- buildClient({ tracingUrl: 'test', servicesUrl: 'test', operationsUrl: 'test' }),
- ).toThrow(e);
- expect(() =>
- buildClient({ provisioningUrl: 'test', servicesUrl: 'test', operationsUrl: 'test' }),
- ).toThrow(e);
- expect(() =>
- buildClient({ provisioningUrl: 'test', tracingUrl: 'test', operationsUrl: 'test' }),
- ).toThrow(e);
+ it('throws is option is missing', () => {
+ expect(() => buildClient()).toThrow(new Error('No options object provided'));
+ });
+ it.each(Object.keys(apiConfig))('throws if %s is missing', (param) => {
+ const e = new Error(`${param} param must be a string`);
+
expect(() =>
- buildClient({ provisioningUrl: 'test', tracingUrl: 'test', servicesUrl: 'test' }),
+ buildClient({
+ ...apiConfig,
+ [param]: undefined,
+ }),
).toThrow(e);
- expect(() => buildClient({})).toThrow(e);
});
});
- describe('isTracingEnabled', () => {
+ describe('isObservabilityEnabled', () => {
it('returns true if requests succeedes', async () => {
axiosMock.onGet(provisioningUrl).reply(200, {
status: 'ready',
});
- const enabled = await client.isTracingEnabled();
+ const enabled = await client.isObservabilityEnabled();
expect(enabled).toBe(true);
});
@@ -67,7 +76,7 @@ describe('buildClient', () => {
it('returns false if response is 404', async () => {
axiosMock.onGet(provisioningUrl).reply(404);
- const enabled = await client.isTracingEnabled();
+ const enabled = await client.isObservabilityEnabled();
expect(enabled).toBe(false);
});
@@ -79,7 +88,7 @@ describe('buildClient', () => {
status: 'not ready',
});
- const enabled = await client.isTracingEnabled();
+ const enabled = await client.isObservabilityEnabled();
expect(enabled).toBe(true);
});
@@ -88,20 +97,20 @@ describe('buildClient', () => {
axiosMock.onGet(provisioningUrl).reply(500);
const e = 'Request failed with status code 500';
- await expect(client.isTracingEnabled()).rejects.toThrow(e);
- expect(Sentry.captureException).toHaveBeenCalledWith(new Error(e));
+ await expect(client.isObservabilityEnabled()).rejects.toThrow(e);
+ expectErrorToBeReported(new Error(e));
});
it('throws in case of unexpected response', async () => {
axiosMock.onGet(provisioningUrl).reply(200, {});
const e = 'Failed to check provisioning';
- await expect(client.isTracingEnabled()).rejects.toThrow(e);
- expect(Sentry.captureException).toHaveBeenCalledWith(new Error(e));
+ await expect(client.isObservabilityEnabled()).rejects.toThrow(e);
+ expectErrorToBeReported(new Error(e));
});
});
- describe('enableTraces', () => {
+ describe('enableObservability', () => {
it('makes a PUT request to the provisioning URL', async () => {
let putConfig;
axiosMock.onPut(provisioningUrl).reply((config) => {
@@ -109,7 +118,7 @@ describe('buildClient', () => {
return [200];
});
- await client.enableTraces();
+ await client.enableObservability();
expect(putConfig.withCredentials).toBe(true);
});
@@ -119,52 +128,32 @@ describe('buildClient', () => {
const e = 'Request failed with status code 401';
- await expect(client.enableTraces()).rejects.toThrow(e);
- expect(Sentry.captureException).toHaveBeenCalledWith(new Error(e));
+ await expect(client.enableObservability()).rejects.toThrow(e);
+ expectErrorToBeReported(new Error(e));
});
});
describe('fetchTrace', () => {
it('fetches the trace from the tracing URL', async () => {
- const mockTraces = [
- {
- trace_id: 'trace-1',
- duration_nano: 3000,
- spans: [{ duration_nano: 1000 }, { duration_nano: 2000 }],
- },
- ];
-
- axiosMock.onGet(tracingUrl).reply(200, {
- traces: mockTraces,
- });
+ const mockTrace = {
+ trace_id: 'trace-1',
+ duration_nano: 3000,
+ spans: [{ duration_nano: 1000 }, { duration_nano: 2000 }],
+ };
+ axiosMock.onGet(`${tracingUrl}/trace-1`).reply(200, mockTrace);
const result = await client.fetchTrace('trace-1');
expect(axios.get).toHaveBeenCalledTimes(1);
- expect(axios.get).toHaveBeenCalledWith(tracingUrl, {
+ expect(axios.get).toHaveBeenCalledWith(`${tracingUrl}/trace-1`, {
withCredentials: true,
- params: { trace_id: 'trace-1' },
});
- expect(result).toEqual(mockTraces[0]);
+ expect(result).toEqual(mockTrace);
});
it('rejects if trace id is missing', () => {
return expect(client.fetchTrace()).rejects.toThrow('traceId is required.');
});
-
- it('rejects if traces are empty', async () => {
- axiosMock.onGet(tracingUrl).reply(200, { traces: [] });
-
- await expect(client.fetchTrace('trace-1')).rejects.toThrow(FETCHING_TRACES_ERROR);
- expect(Sentry.captureException).toHaveBeenCalledWith(new Error(FETCHING_TRACES_ERROR));
- });
-
- it('rejects if traces are invalid', async () => {
- axiosMock.onGet(tracingUrl).reply(200, { traces: 'invalid' });
-
- await expect(client.fetchTraces()).rejects.toThrow(FETCHING_TRACES_ERROR);
- expect(Sentry.captureException).toHaveBeenCalledWith(new Error(FETCHING_TRACES_ERROR));
- });
});
describe('fetchTraces', () => {
@@ -187,7 +176,7 @@ describe('buildClient', () => {
expect(axios.get).toHaveBeenCalledTimes(1);
expect(axios.get).toHaveBeenCalledWith(tracingUrl, {
withCredentials: true,
- params: new URLSearchParams(),
+ params: expect.any(URLSearchParams),
});
expect(result).toEqual(mockResponse);
});
@@ -196,41 +185,64 @@ describe('buildClient', () => {
axiosMock.onGet(tracingUrl).reply(200, {});
await expect(client.fetchTraces()).rejects.toThrow(FETCHING_TRACES_ERROR);
- expect(Sentry.captureException).toHaveBeenCalledWith(new Error(FETCHING_TRACES_ERROR));
+ expectErrorToBeReported(new Error(FETCHING_TRACES_ERROR));
});
it('rejects if traces are invalid', async () => {
axiosMock.onGet(tracingUrl).reply(200, { traces: 'invalid' });
await expect(client.fetchTraces()).rejects.toThrow(FETCHING_TRACES_ERROR);
- expect(Sentry.captureException).toHaveBeenCalledWith(new Error(FETCHING_TRACES_ERROR));
+ expectErrorToBeReported(new Error(FETCHING_TRACES_ERROR));
});
- describe('query filter', () => {
+ describe('sort order', () => {
beforeEach(() => {
axiosMock.onGet(tracingUrl).reply(200, {
traces: [],
});
});
+ it('appends sort param if specified', async () => {
+ await client.fetchTraces({ sortBy: SORTING_OPTIONS.DURATION_DESC });
+
+ expect(getQueryParam()).toBe(`sort=${SORTING_OPTIONS.DURATION_DESC}`);
+ });
+
+ it('defaults to DEFAULT_SORTING_OPTION if no sortBy param is specified', async () => {
+ await client.fetchTraces();
+
+ expect(getQueryParam()).toBe(`sort=${DEFAULT_SORTING_OPTION}`);
+ });
- const getQueryParam = () => decodeURIComponent(axios.get.mock.calls[0][1].params.toString());
+ it('defaults to timestamp_desc if sortBy param is not an accepted value', async () => {
+ await client.fetchTraces({ sortBy: 'foo-bar' });
+
+ expect(getQueryParam()).toBe(`sort=${SORTING_OPTIONS.TIMESTAMP_DESC}`);
+ });
+ });
+
+ describe('query filter', () => {
+ beforeEach(() => {
+ axiosMock.onGet(tracingUrl).reply(200, {
+ traces: [],
+ });
+ });
it('does not set any query param without filters', async () => {
await client.fetchTraces();
- expect(getQueryParam()).toBe('');
+ expect(getQueryParam()).toBe(`sort=${SORTING_OPTIONS.TIMESTAMP_DESC}`);
});
it('appends page_token if specified', async () => {
await client.fetchTraces({ pageToken: 'page-token' });
- expect(getQueryParam()).toBe('page_token=page-token');
+ expect(getQueryParam()).toContain('page_token=page-token');
});
it('appends page_size if specified', async () => {
await client.fetchTraces({ pageSize: 10 });
- expect(getQueryParam()).toBe('page_size=10');
+ expect(getQueryParam()).toContain('page_size=10');
});
it('converts filter to proper query params', async () => {
@@ -244,7 +256,7 @@ describe('buildClient', () => {
{ operator: '=', value: 'op' },
{ operator: '!=', value: 'not-op' },
],
- serviceName: [
+ service: [
{ operator: '=', value: 'service' },
{ operator: '!=', value: 'not-service' },
],
@@ -253,14 +265,16 @@ describe('buildClient', () => {
{ operator: '=', value: 'trace-id' },
{ operator: '!=', value: 'not-trace-id' },
],
+ attribute: [{ operator: '=', value: 'name1=value1' }],
},
});
- expect(getQueryParam()).toBe(
+ expect(getQueryParam()).toContain(
'gt[duration_nano]=100000000&lt[duration_nano]=1000000000' +
'&operation=op&not[operation]=not-op' +
'&service_name=service&not[service_name]=not-service' +
'&period=5m' +
- '&trace_id=trace-id&not[trace_id]=not-trace-id',
+ '&trace_id=trace-id&not[trace_id]=not-trace-id' +
+ '&attr_name=name1&attr_value=value1',
);
});
@@ -273,7 +287,7 @@ describe('buildClient', () => {
],
},
});
- expect(getQueryParam()).toBe('operation=op&operation=op2');
+ expect(getQueryParam()).toContain('operation=op&operation=op2');
});
it('ignores unsupported filters', async () => {
@@ -283,7 +297,7 @@ describe('buildClient', () => {
},
});
- expect(getQueryParam()).toBe('');
+ expect(getQueryParam()).toBe(`sort=${SORTING_OPTIONS.TIMESTAMP_DESC}`);
});
it('ignores empty filters', async () => {
@@ -294,7 +308,7 @@ describe('buildClient', () => {
},
});
- expect(getQueryParam()).toBe('');
+ expect(getQueryParam()).toBe(`sort=${SORTING_OPTIONS.TIMESTAMP_DESC}`);
});
it('ignores unsupported operators', async () => {
@@ -309,7 +323,7 @@ describe('buildClient', () => {
{ operator: '>', value: 'foo' },
{ operator: '<', value: 'foo' },
],
- serviceName: [
+ service: [
{ operator: '>', value: 'foo' },
{ operator: '<', value: 'foo' },
],
@@ -321,7 +335,7 @@ describe('buildClient', () => {
},
});
- expect(getQueryParam()).toBe('');
+ expect(getQueryParam()).toBe(`sort=${SORTING_OPTIONS.TIMESTAMP_DESC}`);
});
});
});
@@ -348,7 +362,7 @@ describe('buildClient', () => {
const e = 'failed to fetch services. invalid response';
await expect(client.fetchServices()).rejects.toThrow(e);
- expect(Sentry.captureException).toHaveBeenCalledWith(new Error(e));
+ expectErrorToBeReported(new Error(e));
});
});
@@ -375,19 +389,17 @@ describe('buildClient', () => {
it('rejects if serviceName is missing', async () => {
const e = 'fetchOperations() - serviceName is required.';
await expect(client.fetchOperations()).rejects.toThrow(e);
- expect(Sentry.captureException).toHaveBeenCalledWith(new Error(e));
+ expectErrorToBeReported(new Error(e));
});
it('rejects if operationUrl does not contain $SERVICE_NAME$', async () => {
client = buildClient({
- tracingUrl,
- provisioningUrl,
- servicesUrl,
+ ...apiConfig,
operationsUrl: 'something',
});
const e = 'fetchOperations() - operationsUrl must contain $SERVICE_NAME$';
await expect(client.fetchOperations(serviceName)).rejects.toThrow(e);
- expect(Sentry.captureException).toHaveBeenCalledWith(new Error(e));
+ expectErrorToBeReported(new Error(e));
});
it('rejects if operations are missing', async () => {
@@ -395,7 +407,44 @@ describe('buildClient', () => {
const e = 'failed to fetch operations. invalid response';
await expect(client.fetchOperations(serviceName)).rejects.toThrow(e);
- expect(Sentry.captureException).toHaveBeenCalledWith(new Error(e));
+ expectErrorToBeReported(new Error(e));
+ });
+ });
+
+ describe('fetchMetrics', () => {
+ const FETCHING_METRICS_ERROR = 'metrics are missing/invalid in the response';
+
+ it('fetches metrics from the metrics URL', async () => {
+ const mockResponse = {
+ metrics: [
+ { name: 'metric A', description: 'a counter metric called A', type: 'COUNTER' },
+ { name: 'metric B', description: 'a gauge metric called B', type: 'GAUGE' },
+ ],
+ };
+
+ axiosMock.onGet(metricsUrl).reply(200, mockResponse);
+
+ const result = await client.fetchMetrics();
+
+ expect(axios.get).toHaveBeenCalledTimes(1);
+ expect(axios.get).toHaveBeenCalledWith(metricsUrl, {
+ withCredentials: true,
+ });
+ expect(result).toEqual(mockResponse);
+ });
+
+ it('rejects if metrics are missing', async () => {
+ axiosMock.onGet(metricsUrl).reply(200, {});
+
+ await expect(client.fetchMetrics()).rejects.toThrow(FETCHING_METRICS_ERROR);
+ expectErrorToBeReported(new Error(FETCHING_METRICS_ERROR));
+ });
+
+ it('rejects if metrics are invalid', async () => {
+ axiosMock.onGet(metricsUrl).reply(200, { traces: 'invalid' });
+
+ await expect(client.fetchMetrics()).rejects.toThrow(FETCHING_METRICS_ERROR);
+ expectErrorToBeReported(new Error(FETCHING_METRICS_ERROR));
});
});
});
diff --git a/spec/frontend/observability/skeleton_spec.js b/spec/frontend/observability/loader_spec.js
index 5501fa117e0..abd1e6f3fe0 100644
--- a/spec/frontend/observability/skeleton_spec.js
+++ b/spec/frontend/observability/loader_spec.js
@@ -1,12 +1,10 @@
import { nextTick } from 'vue';
-import { GlSkeletonLoader, GlAlert, GlLoadingIcon } from '@gitlab/ui';
+import { GlAlert, GlLoadingIcon } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import Loader from '~/observability/components/loader/index.vue';
+import { DEFAULT_TIMERS, CONTENT_STATE } from '~/observability/components/loader/constants';
-import Skeleton from '~/observability/components/skeleton/index.vue';
-
-import { DEFAULT_TIMERS } from '~/observability/constants';
-
-describe('Skeleton component', () => {
+describe('Loader component', () => {
let wrapper;
const findSpinner = () => wrapper.findComponent(GlLoadingIcon);
@@ -16,18 +14,18 @@ describe('Skeleton component', () => {
const findAlert = () => wrapper.findComponent(GlAlert);
const mountComponent = ({ ...props } = {}) => {
- wrapper = shallowMountExtended(Skeleton, {
+ wrapper = shallowMountExtended(Loader, {
propsData: props,
});
};
describe('on mount', () => {
beforeEach(() => {
- mountComponent({ variant: 'spinner' });
+ mountComponent();
});
describe('showing content', () => {
- it('shows the skeleton if content is not loaded within CONTENT_WAIT_MS', async () => {
+ it('shows the loader if content is not loaded within CONTENT_WAIT_MS', async () => {
expect(findSpinner().exists()).toBe(false);
expect(findContentWrapper().exists()).toBe(false);
@@ -39,13 +37,11 @@ describe('Skeleton component', () => {
expect(findContentWrapper().exists()).toBe(false);
});
- it('does not show the skeleton if content loads within CONTENT_WAIT_MS', async () => {
+ it('does not show the loader if content loads within CONTENT_WAIT_MS', async () => {
expect(findSpinner().exists()).toBe(false);
expect(findContentWrapper().exists()).toBe(false);
- wrapper.vm.onContentLoaded();
-
- await nextTick();
+ await wrapper.setProps({ contentState: CONTENT_STATE.LOADED });
expect(findContentWrapper().exists()).toBe(true);
expect(findSpinner().exists()).toBe(false);
@@ -58,7 +54,7 @@ describe('Skeleton component', () => {
expect(findSpinner().exists()).toBe(false);
});
- it('hides the skeleton after content loads', async () => {
+ it('hides the loader after content loads', async () => {
jest.advanceTimersByTime(DEFAULT_TIMERS.CONTENT_WAIT_MS);
await nextTick();
@@ -66,9 +62,7 @@ describe('Skeleton component', () => {
expect(findSpinner().exists()).toBe(true);
expect(findContentWrapper().exists()).toBe(false);
- wrapper.vm.onContentLoaded();
-
- await nextTick();
+ await wrapper.setProps({ contentState: CONTENT_STATE.LOADED });
expect(findContentWrapper().exists()).toBe(true);
expect(findSpinner().exists()).toBe(false);
@@ -89,16 +83,14 @@ describe('Skeleton component', () => {
it('shows the error dialog if content fails to load', async () => {
expect(findAlert().exists()).toBe(false);
- wrapper.vm.onError();
-
- await nextTick();
+ await wrapper.setProps({ contentState: 'error' });
expect(findAlert().exists()).toBe(true);
expect(findContentWrapper().exists()).toBe(false);
});
it('does not show the error dialog if content has loaded within TIMEOUT_MS', async () => {
- wrapper.vm.onContentLoaded();
+ wrapper.setProps({ contentState: CONTENT_STATE.LOADED });
jest.advanceTimersByTime(DEFAULT_TIMERS.TIMEOUT_MS);
await nextTick();
@@ -108,37 +100,4 @@ describe('Skeleton component', () => {
});
});
});
-
- describe('skeleton variant', () => {
- it('shows only the spinner variant when variant is spinner', async () => {
- mountComponent({ variant: 'spinner' });
- jest.advanceTimersByTime(DEFAULT_TIMERS.CONTENT_WAIT_MS);
- await nextTick();
-
- expect(findSpinner().exists()).toBe(true);
- expect(wrapper.findComponent(GlSkeletonLoader).exists()).toBe(false);
- });
-
- it('shows only the default variant when variant is not spinner', async () => {
- mountComponent({ variant: 'unknown' });
- jest.advanceTimersByTime(DEFAULT_TIMERS.CONTENT_WAIT_MS);
- await nextTick();
-
- expect(findSpinner().exists()).toBe(false);
- expect(wrapper.findComponent(GlSkeletonLoader).exists()).toBe(true);
- });
- });
-
- describe('on destroy', () => {
- it('should clear init timer and timeout timer', () => {
- jest.spyOn(global, 'clearTimeout');
- mountComponent();
- wrapper.destroy();
- expect(clearTimeout).toHaveBeenCalledTimes(2);
- expect(clearTimeout.mock.calls).toEqual([
- [wrapper.vm.loadingTimeout], // First call
- [wrapper.vm.errorTimeout], // Second call
- ]);
- });
- });
});
diff --git a/spec/frontend/observability/observability_container_spec.js b/spec/frontend/observability/observability_container_spec.js
index 5d838756308..41906b2f45d 100644
--- a/spec/frontend/observability/observability_container_spec.js
+++ b/spec/frontend/observability/observability_container_spec.js
@@ -1,41 +1,43 @@
import { nextTick } from 'vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import { stubComponent } from 'helpers/stub_component';
import ObservabilityContainer from '~/observability/components/observability_container.vue';
-import ObservabilitySkeleton from '~/observability/components/skeleton/index.vue';
+import ObservabilityLoader from '~/observability/components/loader/index.vue';
+import { CONTENT_STATE } from '~/observability/components/loader/constants';
import { buildClient } from '~/observability/client';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
+import { logError } from '~/lib/logger';
jest.mock('~/observability/client');
+jest.mock('~/sentry/sentry_browser_wrapper');
+jest.mock('~/lib/logger');
describe('ObservabilityContainer', () => {
let wrapper;
- const mockSkeletonOnContentLoaded = jest.fn();
- const mockSkeletonOnError = jest.fn();
-
const OAUTH_URL = 'https://example.com/oauth';
const TRACING_URL = 'https://example.com/tracing';
const PROVISIONING_URL = 'https://example.com/provisioning';
const SERVICES_URL = 'https://example.com/services';
const OPERATIONS_URL = 'https://example.com/operations';
+ const METRICS_URL = 'https://example.com/metrics';
+
+ const mockClient = { mock: 'client' };
beforeEach(() => {
jest.spyOn(console, 'error').mockImplementation();
- buildClient.mockReturnValue({});
+ buildClient.mockReturnValue(mockClient);
wrapper = shallowMountExtended(ObservabilityContainer, {
propsData: {
- oauthUrl: OAUTH_URL,
- tracingUrl: TRACING_URL,
- provisioningUrl: PROVISIONING_URL,
- servicesUrl: SERVICES_URL,
- operationsUrl: OPERATIONS_URL,
- },
- stubs: {
- ObservabilitySkeleton: stubComponent(ObservabilitySkeleton, {
- methods: { onContentLoaded: mockSkeletonOnContentLoaded, onError: mockSkeletonOnError },
- }),
+ apiConfig: {
+ oauthUrl: OAUTH_URL,
+ tracingUrl: TRACING_URL,
+ provisioningUrl: PROVISIONING_URL,
+ servicesUrl: SERVICES_URL,
+ operationsUrl: OPERATIONS_URL,
+ metricssUrl: METRICS_URL,
+ },
},
slots: {
default: {
@@ -43,12 +45,6 @@ describe('ObservabilityContainer', () => {
h(`<div>mockedComponent</div>`);
},
name: 'MockComponent',
- props: {
- observabilityClient: {
- type: Object,
- required: true,
- },
- },
},
},
});
@@ -60,6 +56,8 @@ describe('ObservabilityContainer', () => {
data: {
type: 'AUTH_COMPLETION',
status,
+ message: 'test-message',
+ statusCode: 'test-code',
},
origin: origin ?? new URL(OAUTH_URL).origin,
}),
@@ -67,6 +65,7 @@ describe('ObservabilityContainer', () => {
const findIframe = () => wrapper.findByTestId('observability-oauth-iframe');
const findSlotComponent = () => wrapper.findComponent({ name: 'MockComponent' });
+ const findLoader = () => wrapper.findComponent(ObservabilityLoader);
it('should render the oauth iframe', () => {
const iframe = findIframe();
@@ -76,56 +75,87 @@ describe('ObservabilityContainer', () => {
expect(iframe.attributes('sandbox')).toBe('allow-same-origin allow-forms allow-scripts');
});
- it('should render the ObservabilitySkeleton', () => {
- const skeleton = wrapper.findComponent(ObservabilitySkeleton);
- expect(skeleton.exists()).toBe(true);
+ it('should render the ObservabilityLoader', () => {
+ expect(findLoader().exists()).toBe(true);
});
it('should not render the default slot', () => {
expect(findSlotComponent().exists()).toBe(false);
});
- it('renders the slot content and removes the iframe on oauth success message', async () => {
- dispatchMessageEvent('success');
+ it('should not emit observability-client-ready', () => {
+ expect(wrapper.emitted('observability-client-ready')).toBeUndefined();
+ });
- await nextTick();
+ describe('on oauth success message', () => {
+ beforeEach(async () => {
+ dispatchMessageEvent('success');
- expect(mockSkeletonOnContentLoaded).toHaveBeenCalledTimes(1);
+ await nextTick();
+ });
+
+ it('sets the loader contentState to LOADED', () => {
+ expect(findLoader().props('contentState')).toBe(CONTENT_STATE.LOADED);
+ });
+
+ it('renders the slot content', () => {
+ const slotComponent = findSlotComponent();
+ expect(slotComponent.exists()).toBe(true);
+ });
+
+ it('build the observability client', () => {
+ expect(buildClient).toHaveBeenCalledWith(wrapper.props('apiConfig'));
+ });
- const slotComponent = findSlotComponent();
- expect(slotComponent.exists()).toBe(true);
- expect(buildClient).toHaveBeenCalledWith({
- provisioningUrl: PROVISIONING_URL,
- tracingUrl: TRACING_URL,
- servicesUrl: SERVICES_URL,
- operationsUrl: OPERATIONS_URL,
+ it('emits observability-client-ready', () => {
+ expect(wrapper.emitted('observability-client-ready')).toEqual([[mockClient]]);
});
- expect(findIframe().exists()).toBe(false);
});
- it('does not render the slot content and removes the iframe on oauth error message', async () => {
- dispatchMessageEvent('error');
+ describe('on oauth error message', () => {
+ beforeEach(async () => {
+ dispatchMessageEvent('error');
- await nextTick();
+ await nextTick();
+ });
- expect(mockSkeletonOnError).toHaveBeenCalledTimes(1);
+ it('set the loader contentState to ERROR', () => {
+ expect(findLoader().props('contentState')).toBe(CONTENT_STATE.ERROR);
+ });
- expect(findSlotComponent().exists()).toBe(false);
- expect(findIframe().exists()).toBe(false);
- expect(buildClient).not.toHaveBeenCalled();
+ it('does not renders the slot content', () => {
+ expect(findSlotComponent().exists()).toBe(false);
+ });
+
+ it('does not build the observability client', () => {
+ expect(buildClient).not.toHaveBeenCalled();
+ });
+
+ it('does not emit observability-client-ready', () => {
+ expect(wrapper.emitted('observability-client-ready')).toBeUndefined();
+ });
+
+ it('reports the error', () => {
+ const e = new Error('GOB auth failed with error: test-message - status: test-code');
+ expect(Sentry.captureException).toHaveBeenCalledWith(e);
+ expect(logError).toHaveBeenCalledWith(e);
+ });
});
- it('handles oauth message only once', () => {
- dispatchMessageEvent('success');
+ it('handles oauth message only once', async () => {
dispatchMessageEvent('success');
+ dispatchMessageEvent('error');
+
+ await nextTick();
- expect(mockSkeletonOnContentLoaded).toHaveBeenCalledTimes(1);
+ expect(buildClient).toHaveBeenCalledTimes(1);
+ expect(findLoader().props('contentState')).toBe(CONTENT_STATE.LOADED);
});
it('only handles messages from the oauth url', () => {
dispatchMessageEvent('success', 'www.fake-url.com');
- expect(mockSkeletonOnContentLoaded).toHaveBeenCalledTimes(0);
+ expect(findLoader().props('contentState')).toBe(null);
expect(findSlotComponent().exists()).toBe(false);
expect(findIframe().exists()).toBe(true);
});
@@ -135,6 +165,6 @@ describe('ObservabilityContainer', () => {
dispatchMessageEvent('success');
- expect(mockSkeletonOnContentLoaded).toHaveBeenCalledTimes(0);
+ expect(findLoader().props('contentState')).toBe(null);
});
});
diff --git a/spec/frontend/observability/observability_empty_state_spec.js b/spec/frontend/observability/observability_empty_state_spec.js
new file mode 100644
index 00000000000..ce420dd076d
--- /dev/null
+++ b/spec/frontend/observability/observability_empty_state_spec.js
@@ -0,0 +1,36 @@
+import { GlButton, GlEmptyState } from '@gitlab/ui';
+import ObservabilityEmptyState from '~/observability/components/observability_empty_state.vue';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+
+describe('ObservabilityEmptyState', () => {
+ let wrapper;
+
+ const findEnableButton = () => wrapper.findComponent(GlButton);
+
+ beforeEach(() => {
+ wrapper = shallowMountExtended(ObservabilityEmptyState);
+ });
+
+ it('passes the correct title', () => {
+ expect(wrapper.findComponent(GlEmptyState).props('title')).toBe(
+ 'Get started with GitLab Observability',
+ );
+ });
+
+ it('displays the correct description', () => {
+ const description = wrapper.find('span').text();
+ expect(description).toBe('Monitor your applications with GitLab Observability.');
+ });
+
+ it('displays the enable button', () => {
+ const enableButton = findEnableButton();
+ expect(enableButton.exists()).toBe(true);
+ expect(enableButton.text()).toBe('Enable');
+ });
+
+ it('emits enable-tracing when enable button is clicked', () => {
+ findEnableButton().vm.$emit('click');
+
+ expect(wrapper.emitted('enable-observability')).toHaveLength(1);
+ });
+});
diff --git a/spec/frontend/observability/provisioned_observability_container_spec.js b/spec/frontend/observability/provisioned_observability_container_spec.js
new file mode 100644
index 00000000000..a2e8b60dc9f
--- /dev/null
+++ b/spec/frontend/observability/provisioned_observability_container_spec.js
@@ -0,0 +1,156 @@
+import { GlLoadingIcon } from '@gitlab/ui';
+import ProvisionedObservabilityContainer from '~/observability/components/provisioned_observability_container.vue';
+import ObservabilityContainer from '~/observability/components/observability_container.vue';
+import ObservabilityEmptyState from '~/observability/components/observability_empty_state.vue';
+import waitForPromises from 'helpers/wait_for_promises';
+
+import { createAlert } from '~/alert';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+
+jest.mock('~/alert');
+
+describe('ProvisionedObservabilityContainer', () => {
+ let wrapper;
+ let mockClient;
+
+ const mockClientReady = async () => {
+ await wrapper
+ .findComponent(ObservabilityContainer)
+ .vm.$emit('observability-client-ready', mockClient);
+ };
+
+ const mockClientReadyAndWait = async () => {
+ await wrapper
+ .findComponent(ObservabilityContainer)
+ .vm.$emit('observability-client-ready', mockClient);
+ await waitForPromises();
+ };
+
+ const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
+ const findEmptyState = () => wrapper.findComponent(ObservabilityEmptyState);
+ const findSlotComponent = () => wrapper.findComponent({ name: 'MockComponent' });
+
+ const props = {
+ apiConfig: {
+ oauthUrl: 'https://example.com/oauth',
+ tracingUrl: 'https://example.com/tracing',
+ servicesUrl: 'https://example.com/services',
+ provisioningUrl: 'https://example.com/provisioning',
+ operationsUrl: 'https://example.com/operations',
+ metricsUrl: 'https://example.com/metrics',
+ },
+ };
+
+ beforeEach(() => {
+ mockClient = {
+ isObservabilityEnabled: jest.fn().mockResolvedValue(true),
+ enableObservability: jest.fn().mockResolvedValue(true),
+ };
+ wrapper = shallowMountExtended(ProvisionedObservabilityContainer, {
+ propsData: props,
+ slots: {
+ default: {
+ render(h) {
+ h(`<div>mockedComponent</div>`);
+ },
+ name: 'MockComponent',
+ },
+ },
+ });
+ });
+
+ it('renders the observability-container', () => {
+ const observabilityContainer = wrapper.findComponent(ObservabilityContainer);
+ expect(observabilityContainer.exists()).toBe(true);
+ expect(observabilityContainer.props('apiConfig')).toStrictEqual(props.apiConfig);
+ });
+
+ describe('when the client is ready', () => {
+ it('renders the loading indicator while checking if observability is enabled', async () => {
+ await mockClientReady();
+
+ expect(findLoadingIcon().exists()).toBe(true);
+ expect(findEmptyState().exists()).toBe(false);
+ expect(findSlotComponent().exists()).toBe(false);
+ expect(mockClient.isObservabilityEnabled).toHaveBeenCalledTimes(1);
+ });
+
+ describe('if observability is enabled', () => {
+ beforeEach(async () => {
+ mockClient.isObservabilityEnabled.mockResolvedValue(true);
+ await mockClientReadyAndWait();
+ });
+
+ it('renders the content slot', () => {
+ expect(findLoadingIcon().exists()).toBe(false);
+ expect(findEmptyState().exists()).toBe(false);
+ expect(findSlotComponent().exists()).toBe(true);
+ });
+ });
+
+ describe('if observability is not enabled', () => {
+ beforeEach(async () => {
+ mockClient.isObservabilityEnabled.mockResolvedValue(false);
+ await mockClientReadyAndWait();
+ });
+
+ it('renders the empty state', () => {
+ expect(findLoadingIcon().exists()).toBe(false);
+ expect(findEmptyState().exists()).toBe(true);
+ expect(findSlotComponent().exists()).toBe(false);
+ });
+
+ describe('when empty-state emits enable-observability', () => {
+ it('shows the loading icon', async () => {
+ await findEmptyState().vm.$emit('enable-observability');
+
+ expect(findLoadingIcon().exists()).toBe(true);
+ });
+
+ it('enable observability', async () => {
+ await findEmptyState().vm.$emit('enable-observability');
+
+ expect(mockClient.enableObservability).toHaveBeenCalledTimes(1);
+ });
+
+ it('shows the content slot', async () => {
+ await findEmptyState().vm.$emit('enable-observability');
+ await waitForPromises();
+
+ expect(findLoadingIcon().exists()).toBe(false);
+ expect(findEmptyState().exists()).toBe(false);
+ expect(findSlotComponent().exists()).toBe(true);
+ });
+ });
+ });
+ });
+
+ describe('error handling', () => {
+ it('shows an alert if checking if observability is enabled fails', async () => {
+ mockClient.isObservabilityEnabled.mockRejectedValue(new Error('error'));
+
+ await mockClientReadyAndWait();
+
+ expect(findLoadingIcon().exists()).toBe(false);
+ expect(findEmptyState().exists()).toBe(false);
+ expect(findSlotComponent().exists()).toBe(false);
+ expect(createAlert).toHaveBeenLastCalledWith({
+ message: 'Error: Failed to load page. Try reloading the page.',
+ });
+ });
+
+ it('shows an alert when checking if observability is enabled fails', async () => {
+ mockClient.isObservabilityEnabled.mockResolvedValue(false);
+ mockClient.enableObservability.mockRejectedValue(new Error('error'));
+
+ await mockClientReadyAndWait();
+
+ await findEmptyState().vm.$emit('enable-observability');
+ await waitForPromises();
+
+ expect(createAlert).toHaveBeenLastCalledWith({
+ message: 'Error: Failed to enable GitLab Observability. Please retry later.',
+ });
+ });
+ });
+});
diff --git a/spec/frontend/organizations/new/components/app_spec.js b/spec/frontend/organizations/new/components/app_spec.js
index 06d30ad6b12..4f31baedbf6 100644
--- a/spec/frontend/organizations/new/components/app_spec.js
+++ b/spec/frontend/organizations/new/components/app_spec.js
@@ -3,16 +3,19 @@ import Vue, { nextTick } from 'vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import App from '~/organizations/new/components/app.vue';
-import resolvers from '~/organizations/shared/graphql/resolvers';
+import organizationCreateMutation from '~/organizations/new/graphql/mutations/organization_create.mutation.graphql';
import NewEditForm from '~/organizations/shared/components/new_edit_form.vue';
import { visitUrlWithAlerts } from '~/lib/utils/url_utility';
-import { createOrganizationResponse } from '~/organizations/mock_data';
+import FormErrorsAlert from '~/vue_shared/components/form/errors_alert.vue';
+import {
+ organizationCreateResponse,
+ organizationCreateResponseWithErrors,
+} from '~/organizations/mock_data';
import { createAlert } from '~/alert';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
Vue.use(VueApollo);
-jest.useFakeTimers();
jest.mock('~/lib/utils/url_utility');
jest.mock('~/alert');
@@ -21,8 +24,12 @@ describe('OrganizationNewApp', () => {
let wrapper;
let mockApollo;
- const createComponent = ({ mockResolvers = resolvers } = {}) => {
- mockApollo = createMockApollo([], mockResolvers);
+ const createComponent = ({
+ handlers = [
+ [organizationCreateMutation, jest.fn().mockResolvedValue(organizationCreateResponse)],
+ ],
+ } = {}) => {
+ mockApollo = createMockApollo(handlers);
wrapper = shallowMountExtended(App, { apolloProvider: mockApollo });
};
@@ -46,13 +53,11 @@ describe('OrganizationNewApp', () => {
describe('when form is submitted', () => {
describe('when API is loading', () => {
beforeEach(async () => {
- const mockResolvers = {
- Mutation: {
- createOrganization: jest.fn().mockReturnValueOnce(new Promise(() => {})),
- },
- };
-
- createComponent({ mockResolvers });
+ createComponent({
+ handlers: [
+ [organizationCreateMutation, jest.fn().mockReturnValueOnce(new Promise(() => {}))],
+ ],
+ });
await submitForm();
});
@@ -66,13 +71,12 @@ describe('OrganizationNewApp', () => {
beforeEach(async () => {
createComponent();
await submitForm();
- jest.runAllTimers();
await waitForPromises();
});
- it('redirects user to organization path', () => {
+ it('redirects user to organization web url', () => {
expect(visitUrlWithAlerts).toHaveBeenCalledWith(
- createOrganizationResponse.organization.path,
+ organizationCreateResponse.data.organizationCreate.organization.webUrl,
[
{
id: 'organization-successfully-created',
@@ -86,26 +90,44 @@ describe('OrganizationNewApp', () => {
});
describe('when API request is not successful', () => {
- const error = new Error();
-
- beforeEach(async () => {
- const mockResolvers = {
- Mutation: {
- createOrganization: jest.fn().mockRejectedValueOnce(error),
- },
- };
+ describe('when there is a network error', () => {
+ const error = new Error();
+
+ beforeEach(async () => {
+ createComponent({
+ handlers: [[organizationCreateMutation, jest.fn().mockRejectedValue(error)]],
+ });
+ await submitForm();
+ await waitForPromises();
+ });
- createComponent({ mockResolvers });
- await submitForm();
- jest.runAllTimers();
- await waitForPromises();
+ it('displays error alert', () => {
+ expect(createAlert).toHaveBeenCalledWith({
+ message: 'An error occurred creating an organization. Please try again.',
+ error,
+ captureError: true,
+ });
+ });
});
- it('displays error alert', () => {
- expect(createAlert).toHaveBeenCalledWith({
- message: 'An error occurred creating an organization. Please try again.',
- error,
- captureError: true,
+ describe('when there are GraphQL errors', () => {
+ beforeEach(async () => {
+ createComponent({
+ handlers: [
+ [
+ organizationCreateMutation,
+ jest.fn().mockResolvedValue(organizationCreateResponseWithErrors),
+ ],
+ ],
+ });
+ await submitForm();
+ await waitForPromises();
+ });
+
+ it('displays form errors alert', () => {
+ expect(wrapper.findComponent(FormErrorsAlert).props('errors')).toEqual(
+ organizationCreateResponseWithErrors.data.organizationCreate.errors,
+ );
});
});
});
diff --git a/spec/frontend/organizations/settings/general/components/app_spec.js b/spec/frontend/organizations/settings/general/components/app_spec.js
new file mode 100644
index 00000000000..6d75f8a9949
--- /dev/null
+++ b/spec/frontend/organizations/settings/general/components/app_spec.js
@@ -0,0 +1,19 @@
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import OrganizationSettings from '~/organizations/settings/general/components/organization_settings.vue';
+import App from '~/organizations/settings/general/components/app.vue';
+
+describe('OrganizationSettings', () => {
+ let wrapper;
+
+ const createComponent = () => {
+ wrapper = shallowMountExtended(App);
+ };
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('renders `Organization settings` section', () => {
+ expect(wrapper.findComponent(OrganizationSettings).exists()).toBe(true);
+ });
+});
diff --git a/spec/frontend/organizations/settings/general/components/organization_settings_spec.js b/spec/frontend/organizations/settings/general/components/organization_settings_spec.js
new file mode 100644
index 00000000000..7645b41e3bd
--- /dev/null
+++ b/spec/frontend/organizations/settings/general/components/organization_settings_spec.js
@@ -0,0 +1,126 @@
+import VueApollo from 'vue-apollo';
+import Vue, { nextTick } from 'vue';
+
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import OrganizationSettings from '~/organizations/settings/general/components/organization_settings.vue';
+import SettingsBlock from '~/vue_shared/components/settings/settings_block.vue';
+import NewEditForm from '~/organizations/shared/components/new_edit_form.vue';
+import { FORM_FIELD_NAME, FORM_FIELD_ID } from '~/organizations/shared/constants';
+import resolvers from '~/organizations/shared/graphql/resolvers';
+import { createAlert, VARIANT_INFO } from '~/alert';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+
+Vue.use(VueApollo);
+jest.useFakeTimers();
+jest.mock('~/alert');
+
+describe('OrganizationSettings', () => {
+ let wrapper;
+ let mockApollo;
+
+ const defaultProvide = {
+ organization: {
+ id: 1,
+ name: 'GitLab',
+ },
+ };
+
+ const createComponent = ({ mockResolvers = resolvers } = {}) => {
+ mockApollo = createMockApollo([], mockResolvers);
+
+ wrapper = shallowMountExtended(OrganizationSettings, {
+ provide: defaultProvide,
+ apolloProvider: mockApollo,
+ });
+ };
+
+ const findForm = () => wrapper.findComponent(NewEditForm);
+ const submitForm = async () => {
+ findForm().vm.$emit('submit', { name: 'Foo bar', path: 'foo-bar' });
+ await nextTick();
+ };
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ afterEach(() => {
+ mockApollo = null;
+ });
+
+ it('renders settings block', () => {
+ expect(wrapper.findComponent(SettingsBlock).exists()).toBe(true);
+ });
+
+ it('renders form with correct props', () => {
+ createComponent();
+
+ expect(findForm().props()).toMatchObject({
+ loading: false,
+ initialFormValues: defaultProvide.organization,
+ fieldsToRender: [FORM_FIELD_NAME, FORM_FIELD_ID],
+ });
+ });
+
+ describe('when form is submitted', () => {
+ describe('when API is loading', () => {
+ beforeEach(async () => {
+ const mockResolvers = {
+ Mutation: {
+ updateOrganization: jest.fn().mockReturnValueOnce(new Promise(() => {})),
+ },
+ };
+
+ createComponent({ mockResolvers });
+
+ await submitForm();
+ });
+
+ it('sets form `loading` prop to `true`', () => {
+ expect(findForm().props('loading')).toBe(true);
+ });
+ });
+
+ describe('when API request is successful', () => {
+ beforeEach(async () => {
+ createComponent();
+ await submitForm();
+ jest.runAllTimers();
+ await waitForPromises();
+ });
+
+ it('displays info alert', () => {
+ expect(createAlert).toHaveBeenCalledWith({
+ message: 'Organization was successfully updated.',
+ variant: VARIANT_INFO,
+ });
+ });
+ });
+
+ describe('when API request is not successful', () => {
+ const error = new Error();
+
+ beforeEach(async () => {
+ const mockResolvers = {
+ Mutation: {
+ updateOrganization: jest.fn().mockRejectedValueOnce(error),
+ },
+ };
+
+ createComponent({ mockResolvers });
+ await submitForm();
+ jest.runAllTimers();
+ await waitForPromises();
+ });
+
+ it('displays error alert', () => {
+ expect(createAlert).toHaveBeenCalledWith({
+ message: 'An error occurred updating your organization. Please try again.',
+ error,
+ captureError: true,
+ });
+ });
+ });
+ });
+});
diff --git a/spec/frontend/organizations/shared/components/new_edit_form_spec.js b/spec/frontend/organizations/shared/components/new_edit_form_spec.js
index 43c099fbb1c..93f022a3259 100644
--- a/spec/frontend/organizations/shared/components/new_edit_form_spec.js
+++ b/spec/frontend/organizations/shared/components/new_edit_form_spec.js
@@ -1,6 +1,7 @@
import { GlButton, GlInputGroupText, GlTruncate } from '@gitlab/ui';
import NewEditForm from '~/organizations/shared/components/new_edit_form.vue';
+import { FORM_FIELD_NAME, FORM_FIELD_ID, FORM_FIELD_PATH } from '~/organizations/shared/constants';
import { mountExtended } from 'helpers/vue_test_utils_helper';
describe('NewEditForm', () => {
@@ -27,6 +28,7 @@ describe('NewEditForm', () => {
};
const findNameField = () => wrapper.findByLabelText('Organization name');
+ const findIdField = () => wrapper.findByLabelText('Organization ID');
const findUrlField = () => wrapper.findByLabelText('Organization URL');
const submitForm = async () => {
await wrapper.findByRole('button', { name: 'Create organization' }).trigger('click');
@@ -47,6 +49,56 @@ describe('NewEditForm', () => {
expect(findUrlField().exists()).toBe(true);
});
+ it('requires `Organization URL` field to be a minimum of two characters', async () => {
+ createComponent();
+
+ await findUrlField().setValue('f');
+ await submitForm();
+
+ expect(
+ wrapper.findByText('Organization URL must be a minimum of two characters.').exists(),
+ ).toBe(true);
+ });
+
+ describe('when `fieldsToRender` prop is set', () => {
+ beforeEach(() => {
+ createComponent({ propsData: { fieldsToRender: [FORM_FIELD_ID] } });
+ });
+
+ it('only renders provided fields', () => {
+ expect(findNameField().exists()).toBe(false);
+ expect(findIdField().exists()).toBe(true);
+ expect(findUrlField().exists()).toBe(false);
+ });
+ });
+
+ describe('when `initialFormValues` prop is set', () => {
+ beforeEach(() => {
+ createComponent({
+ propsData: {
+ fieldsToRender: [FORM_FIELD_NAME, FORM_FIELD_ID, FORM_FIELD_PATH],
+ initialFormValues: {
+ [FORM_FIELD_NAME]: 'Foo bar',
+ [FORM_FIELD_ID]: 1,
+ [FORM_FIELD_PATH]: 'foo-bar',
+ },
+ },
+ });
+ });
+
+ it('sets initial values for fields', () => {
+ expect(findNameField().element.value).toBe('Foo bar');
+ expect(findIdField().element.value).toBe('1');
+ expect(findUrlField().element.value).toBe('foo-bar');
+ });
+ });
+
+ it('renders `Organization ID` field as disabled', () => {
+ createComponent({ propsData: { fieldsToRender: [FORM_FIELD_ID] } });
+
+ expect(findIdField().attributes('disabled')).toBe('disabled');
+ });
+
describe('when form is submitted without filling in required fields', () => {
beforeEach(async () => {
createComponent();
@@ -100,6 +152,30 @@ describe('NewEditForm', () => {
});
});
+ describe('when `Organization URL` field is not rendered', () => {
+ beforeEach(async () => {
+ createComponent({
+ propsData: {
+ fieldsToRender: [FORM_FIELD_NAME, FORM_FIELD_ID],
+ initialFormValues: {
+ [FORM_FIELD_NAME]: 'Foo bar',
+ [FORM_FIELD_ID]: 1,
+ [FORM_FIELD_PATH]: 'foo-bar',
+ },
+ },
+ });
+
+ await findNameField().setValue('Foo bar baz');
+ await submitForm();
+ });
+
+ it('does not modify `Organization URL` when typing in `Organization name`', () => {
+ expect(wrapper.emitted('submit')).toEqual([
+ [{ name: 'Foo bar baz', id: 1, path: 'foo-bar' }],
+ ]);
+ });
+ });
+
describe('when `loading` prop is `true`', () => {
beforeEach(() => {
createComponent({ propsData: { loading: true } });
@@ -109,4 +185,46 @@ describe('NewEditForm', () => {
expect(wrapper.findComponent(GlButton).props('loading')).toBe(true);
});
});
+
+ describe('when `showCancelButton` prop is `false`', () => {
+ beforeEach(() => {
+ createComponent({ propsData: { showCancelButton: false } });
+ });
+
+ it('does not show cancel button', () => {
+ expect(wrapper.findByRole('link', { name: 'Cancel' }).exists()).toBe(false);
+ });
+ });
+
+ describe('when `showCancelButton` prop is `true`', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('shows cancel button', () => {
+ expect(wrapper.findByRole('link', { name: 'Cancel' }).attributes('href')).toBe(
+ defaultProvide.organizationsPath,
+ );
+ });
+ });
+
+ describe('when `submitButtonText` prop is not set', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('defaults to `Create organization`', () => {
+ expect(wrapper.findByRole('button', { name: 'Create organization' }).exists()).toBe(true);
+ });
+ });
+
+ describe('when `submitButtonText` prop is set', () => {
+ beforeEach(() => {
+ createComponent({ propsData: { submitButtonText: 'Save changes' } });
+ });
+
+ it('uses it for submit button', () => {
+ expect(wrapper.findByRole('button', { name: 'Save changes' }).exists()).toBe(true);
+ });
+ });
});
diff --git a/spec/frontend/organizations/users/components/app_spec.js b/spec/frontend/organizations/users/components/app_spec.js
new file mode 100644
index 00000000000..b30fd984099
--- /dev/null
+++ b/spec/frontend/organizations/users/components/app_spec.js
@@ -0,0 +1,81 @@
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import { createAlert } from '~/alert';
+import organizationUsersQuery from '~/organizations/users/graphql/organization_users.query.graphql';
+import OrganizationsUsersApp from '~/organizations/users/components/app.vue';
+import { MOCK_ORGANIZATION_GID, MOCK_USERS } from '../mock_data';
+
+jest.mock('~/alert');
+
+Vue.use(VueApollo);
+
+const mockError = new Error();
+
+const loadingResolver = jest.fn().mockReturnValue(new Promise(() => {}));
+const successfulResolver = (nodes) =>
+ jest.fn().mockResolvedValue({
+ data: { organization: { id: 1, organizationUsers: { nodes } } },
+ });
+const errorResolver = jest.fn().mockRejectedValueOnce(mockError);
+
+describe('OrganizationsUsersApp', () => {
+ let wrapper;
+ let mockApollo;
+
+ const createComponent = (mockResolvers = successfulResolver(MOCK_USERS)) => {
+ mockApollo = createMockApollo([[organizationUsersQuery, mockResolvers]]);
+
+ wrapper = shallowMountExtended(OrganizationsUsersApp, {
+ apolloProvider: mockApollo,
+ provide: {
+ organizationGid: MOCK_ORGANIZATION_GID,
+ },
+ });
+ };
+
+ afterEach(() => {
+ mockApollo = null;
+ });
+
+ const findOrganizationUsersLoading = () => wrapper.findByText('Loading');
+ const findOrganizationUsers = () => wrapper.findByTestId('organization-users');
+
+ describe.each`
+ description | mockResolver | loading | userData | error
+ ${'when API call is loading'} | ${loadingResolver} | ${true} | ${[]} | ${false}
+ ${'when API returns successful with results'} | ${successfulResolver(MOCK_USERS)} | ${false} | ${MOCK_USERS} | ${false}
+ ${'when API returns successful without results'} | ${successfulResolver([])} | ${false} | ${[]} | ${false}
+ ${'when API returns error'} | ${errorResolver} | ${false} | ${[]} | ${true}
+ `('$description', ({ mockResolver, loading, userData, error }) => {
+ beforeEach(async () => {
+ createComponent(mockResolver);
+ await waitForPromises();
+ });
+
+ it(`does ${
+ loading ? '' : 'not '
+ }render the organization users view with loading placeholder`, () => {
+ expect(findOrganizationUsersLoading().exists()).toBe(loading);
+ });
+
+ it(`renders the organization users view with ${
+ userData.length ? 'correct' : 'empty'
+ } users array raw data`, () => {
+ expect(JSON.parse(findOrganizationUsers().text())).toStrictEqual(userData);
+ });
+
+ it(`does ${error ? '' : 'not '}render an error message`, () => {
+ return error
+ ? expect(createAlert).toHaveBeenCalledWith({
+ message:
+ 'An error occurred loading the organization users. Please refresh the page to try again.',
+ error: mockError,
+ captureError: true,
+ })
+ : expect(createAlert).not.toHaveBeenCalled();
+ });
+ });
+});
diff --git a/spec/frontend/organizations/users/mock_data.js b/spec/frontend/organizations/users/mock_data.js
new file mode 100644
index 00000000000..4f159c70c2c
--- /dev/null
+++ b/spec/frontend/organizations/users/mock_data.js
@@ -0,0 +1,22 @@
+export const MOCK_ORGANIZATION_GID = 'gid://gitlab/Organizations::Organization/1';
+
+export const MOCK_USERS = [
+ {
+ badges: [],
+ id: 'gid://gitlab/Organizations::OrganizationUser/3',
+ user: { id: 'gid://gitlab/User/3' },
+ },
+ {
+ badges: [],
+ id: 'gid://gitlab/Organizations::OrganizationUser/2',
+ user: { id: 'gid://gitlab/User/2' },
+ },
+ {
+ badges: [
+ { text: 'Admin', variant: 'success' },
+ { text: "It's you!", variant: 'muted' },
+ ],
+ id: 'gid://gitlab/Organizations::OrganizationUser/1',
+ user: { id: 'gid://gitlab/User/1' },
+ },
+];
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 17acf7381c0..d6c3d98efa3 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
@@ -177,7 +177,7 @@ exports[`PypiInstallation renders all the messages 1`] = `
</div>
</div>
<small
- class="form-text text-muted"
+ class="form-text text-gl-muted"
id="reference-8"
tabindex="-1"
>
diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/additional_metadata_spec.js b/spec/frontend/packages_and_registries/package_registry/components/details/additional_metadata_spec.js
index 2e59c27cc1b..133941bbb2e 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/details/additional_metadata_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/components/details/additional_metadata_spec.js
@@ -1,7 +1,7 @@
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import { GlAlert } from '@gitlab/ui';
-import * as Sentry from '@sentry/browser';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
import {
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 5ba4b1f687e..7823b146aba 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
@@ -6,9 +6,9 @@ import {
GlModal,
GlKeysetPagination,
} from '@gitlab/ui';
-import * as Sentry from '@sentry/browser';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import { stubComponent } from 'helpers/stub_component';
import { mountExtended, extendedWrapper } from 'helpers/vue_test_utils_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/package_history_spec.js b/spec/frontend/packages_and_registries/package_registry/components/details/package_history_spec.js
index ed470f63b8a..d83d571872c 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/details/package_history_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/components/details/package_history_spec.js
@@ -1,7 +1,7 @@
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import { GlAlert, GlLink, GlSprintf } from '@gitlab/ui';
-import * as Sentry from '@sentry/browser';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import { stubComponent } from 'helpers/stub_component';
import createMockApollo from 'helpers/mock_apollo_helper';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/package_versions_list_spec.js b/spec/frontend/packages_and_registries/package_registry/components/details/package_versions_list_spec.js
index e9f2a2c5095..8e22e9a3b0c 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/details/package_versions_list_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/components/details/package_versions_list_spec.js
@@ -1,7 +1,7 @@
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import { GlAlert } from '@gitlab/ui';
-import * as Sentry from '@sentry/browser';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
import { stubComponent } from 'helpers/stub_component';
diff --git a/spec/frontend/packages_and_registries/package_registry/components/list/__snapshots__/publish_method_spec.js.snap b/spec/frontend/packages_and_registries/package_registry/components/list/__snapshots__/publish_method_spec.js.snap
index f202635d717..89cf5acdef3 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/list/__snapshots__/publish_method_spec.js.snap
+++ b/spec/frontend/packages_and_registries/package_registry/components/list/__snapshots__/publish_method_spec.js.snap
@@ -21,7 +21,7 @@ exports[`publish_method renders 1`] = `
size="16"
/>
<gl-link-stub
- class="gl-mr-2"
+ class="gl-mr-2 gl-text-decoration-underline"
data-testid="pipeline-sha"
href="/namespace14/project14/-/commit/b83d6e391c22777fca1ed3012fce84f633d7fed0"
>
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 91dc02f8f39..6c03f91b73d 100644
--- a/spec/frontend/packages_and_registries/package_registry/mock_data.js
+++ b/spec/frontend/packages_and_registries/package_registry/mock_data.js
@@ -308,7 +308,7 @@ export const packageMetadataQuery = (packageType) => {
id: 'gid://gitlab/Packages::Package/111',
packageType,
metadata: {
- ...(packageTypeMetadataQueryMapping[packageType]?.() ?? {}),
+ ...packageTypeMetadataQueryMapping[packageType]?.(),
},
__typename: 'PackageDetailsType',
},
diff --git a/spec/frontend/packages_and_registries/settings/group/components/package_settings_spec.js b/spec/frontend/packages_and_registries/settings/group/components/package_settings_spec.js
index 49e76cfbae0..bf7abd353b6 100644
--- a/spec/frontend/packages_and_registries/settings/group/components/package_settings_spec.js
+++ b/spec/frontend/packages_and_registries/settings/group/components/package_settings_spec.js
@@ -60,13 +60,17 @@ describe('Packages Settings', () => {
const findDescription = () => wrapper.findByTestId('description');
const findMavenSettings = () => wrapper.findByTestId('maven-settings');
const findGenericSettings = () => wrapper.findByTestId('generic-settings');
+ const findNugetSettings = () => wrapper.findByTestId('nuget-settings');
const findMavenDuplicatedSettingsToggle = () => findMavenSettings().findComponent(GlToggle);
const findGenericDuplicatedSettingsToggle = () => findGenericSettings().findComponent(GlToggle);
+ const findNugetDuplicatedSettingsToggle = () => findNugetSettings().findComponent(GlToggle);
const findMavenDuplicatedSettingsExceptionsInput = () =>
findMavenSettings().findComponent(ExceptionsInput);
const findGenericDuplicatedSettingsExceptionsInput = () =>
findGenericSettings().findComponent(ExceptionsInput);
+ const findNugetDuplicatedSettingsExceptionsInput = () =>
+ findNugetSettings().findComponent(ExceptionsInput);
const fillApolloCache = () => {
apolloProvider.defaultClient.cache.writeQuery({
@@ -208,6 +212,58 @@ describe('Packages Settings', () => {
});
});
+ describe('nuget settings', () => {
+ it('exists', () => {
+ mountComponent({ mountFn: mountExtended });
+
+ expect(findNugetSettings().find('td').text()).toBe('NuGet');
+ });
+
+ it('renders toggle', () => {
+ mountComponent({ mountFn: mountExtended });
+
+ const { nugetDuplicatesAllowed } = packageSettings;
+
+ expect(findNugetDuplicatedSettingsToggle().exists()).toBe(true);
+ expect(findNugetDuplicatedSettingsToggle().props()).toMatchObject({
+ label: DUPLICATES_TOGGLE_LABEL,
+ value: nugetDuplicatesAllowed,
+ disabled: false,
+ labelPosition: 'hidden',
+ });
+ });
+
+ it('renders ExceptionsInput and assigns duplication allowness and exception props', () => {
+ mountComponent({ mountFn: mountExtended });
+
+ const { nugetDuplicatesAllowed, nugetDuplicateExceptionRegex } = packageSettings;
+
+ expect(findNugetDuplicatedSettingsExceptionsInput().props()).toMatchObject({
+ duplicatesAllowed: nugetDuplicatesAllowed,
+ duplicateExceptionRegex: nugetDuplicateExceptionRegex,
+ duplicateExceptionRegexError: '',
+ loading: false,
+ name: 'nugetDuplicateExceptionRegex',
+ id: 'nuget-duplicated-settings-regex-input',
+ });
+ });
+
+ it('on update event calls the mutation', () => {
+ const mutationResolver = jest.fn().mockResolvedValue(groupPackageSettingsMutationMock());
+ mountComponent({ mountFn: mountExtended, mutationResolver });
+
+ fillApolloCache();
+
+ findNugetDuplicatedSettingsExceptionsInput().vm.$emit('update', {
+ nugetDuplicateExceptionRegex: ')',
+ });
+
+ expect(mutationResolver).toHaveBeenCalledWith({
+ input: { nugetDuplicateExceptionRegex: ')', namespacePath: 'foo_group_path' },
+ });
+ });
+ });
+
describe('settings update', () => {
describe('success state', () => {
beforeEach(() => {
diff --git a/spec/frontend/packages_and_registries/settings/group/mock_data.js b/spec/frontend/packages_and_registries/settings/group/mock_data.js
index 1ca9dc6daeb..c68b0b8e23f 100644
--- a/spec/frontend/packages_and_registries/settings/group/mock_data.js
+++ b/spec/frontend/packages_and_registries/settings/group/mock_data.js
@@ -3,6 +3,8 @@ const packageDuplicateSettings = {
mavenDuplicateExceptionRegex: '',
genericDuplicatesAllowed: true,
genericDuplicateExceptionRegex: '',
+ nugetDuplicatesAllowed: true,
+ nugetDuplicateExceptionRegex: '',
};
export const packageForwardingSettings = {
diff --git a/spec/frontend/pages/admin/application_settings/appearances/preview_sign_in/index_spec.js b/spec/frontend/pages/admin/application_settings/appearances/preview_sign_in/index_spec.js
new file mode 100644
index 00000000000..2fec65ad4c8
--- /dev/null
+++ b/spec/frontend/pages/admin/application_settings/appearances/preview_sign_in/index_spec.js
@@ -0,0 +1,10 @@
+import { renderGFM } from '~/behaviors/markdown/render_gfm';
+
+jest.mock('~/behaviors/markdown/render_gfm');
+
+describe('Preview sign in', () => {
+ it('calls `renderGFM` to ensure that all gitlab-flavoured markdown is rendered on the preview sign in page', async () => {
+ await import('~/pages/sessions/new/index');
+ expect(renderGFM).toHaveBeenCalledWith(document.body);
+ });
+});
diff --git a/spec/frontend/pages/groups/sso/index_spec.js b/spec/frontend/pages/groups/sso/index_spec.js
new file mode 100644
index 00000000000..3166c4aa743
--- /dev/null
+++ b/spec/frontend/pages/groups/sso/index_spec.js
@@ -0,0 +1,10 @@
+import { renderGFM } from '~/behaviors/markdown/render_gfm';
+
+jest.mock('~/behaviors/markdown/render_gfm');
+
+describe('SAML single sign-on page', () => {
+ it('calls `renderGFM` to ensure that all gitlab-flavoured markdown is rendered on the SAML single sign-on page', async () => {
+ await import('~/pages/sessions/new/index');
+ expect(renderGFM).toHaveBeenCalledWith(document.body);
+ });
+});
diff --git a/spec/frontend/pages/import/bulk_imports/details/index_spec.js b/spec/frontend/pages/import/bulk_imports/details/index_spec.js
new file mode 100644
index 00000000000..0fefa3017f7
--- /dev/null
+++ b/spec/frontend/pages/import/bulk_imports/details/index_spec.js
@@ -0,0 +1,37 @@
+import Vue from 'vue';
+import { initBulkImportDetails } from '~/pages/import/bulk_imports/details/index';
+
+jest.mock('~/import/details/components/bulk_import_details_app.vue');
+
+describe('initBulkImportDetails', () => {
+ let appRoot;
+
+ const createAppRoot = () => {
+ appRoot = document.createElement('div');
+ appRoot.setAttribute('class', 'js-bulk-import-details');
+ document.body.appendChild(appRoot);
+ };
+
+ afterEach(() => {
+ if (appRoot) {
+ appRoot.remove();
+ appRoot = null;
+ }
+ });
+
+ describe('when there is no app root', () => {
+ it('returns null', () => {
+ expect(initBulkImportDetails()).toBeNull();
+ });
+ });
+
+ describe('when there is an app root', () => {
+ beforeEach(() => {
+ createAppRoot();
+ });
+
+ it('returns a Vue instance', () => {
+ expect(initBulkImportDetails()).toBeInstanceOf(Vue);
+ });
+ });
+});
diff --git a/spec/frontend/pages/passwords/new/index_spec.js b/spec/frontend/pages/passwords/new/index_spec.js
new file mode 100644
index 00000000000..084fb317f4e
--- /dev/null
+++ b/spec/frontend/pages/passwords/new/index_spec.js
@@ -0,0 +1,10 @@
+import { renderGFM } from '~/behaviors/markdown/render_gfm';
+
+jest.mock('~/behaviors/markdown/render_gfm');
+
+describe('Password page', () => {
+ it('calls `renderGFM` to ensure that all gitlab-flavoured markdown is rendered on the password page', async () => {
+ await import('~/pages/sessions/new/index');
+ expect(renderGFM).toHaveBeenCalledWith(document.body);
+ });
+});
diff --git a/spec/frontend/pages/projects/forks/new/components/fork_form_spec.js b/spec/frontend/pages/projects/forks/new/components/fork_form_spec.js
index 7bc4cd4d541..b0bfa4620c6 100644
--- a/spec/frontend/pages/projects/forks/new/components/fork_form_spec.js
+++ b/spec/frontend/pages/projects/forks/new/components/fork_form_spec.js
@@ -1,4 +1,11 @@
-import { GlFormInputGroup, GlFormInput, GlForm, GlFormRadioGroup, GlFormRadio } from '@gitlab/ui';
+import {
+ GlFormInputGroup,
+ GlFormInput,
+ GlForm,
+ GlFormRadioGroup,
+ GlFormRadio,
+ GlSprintf,
+} from '@gitlab/ui';
import { getByRole } from '@testing-library/dom';
import { mount, shallowMount } from '@vue/test-utils';
import axios from 'axios';
@@ -41,6 +48,7 @@ describe('ForkForm component', () => {
projectPath: 'project-name',
projectDescription: 'some project description',
projectVisibility: 'private',
+ projectDefaultBranch: 'main',
restrictedVisibilityLevels: [],
};
@@ -96,6 +104,7 @@ describe('ForkForm component', () => {
GlFormInput,
GlFormRadioGroup,
GlFormRadio,
+ GlSprintf,
},
});
};
@@ -118,13 +127,13 @@ describe('ForkForm component', () => {
const findInternalRadio = () => wrapper.find('[data-testid="radio-internal"]');
const findPublicRadio = () => wrapper.find('[data-testid="radio-public"]');
const findForkNameInput = () => wrapper.find('[data-testid="fork-name-input"]');
- const findGlFormRadioGroup = () => wrapper.findComponent(GlFormRadioGroup);
const findForkUrlInput = () => wrapper.findComponent(ProjectNamespace);
const findForkSlugInput = () => wrapper.find('[data-testid="fork-slug-input"]');
const findForkDescriptionTextarea = () =>
wrapper.find('[data-testid="fork-description-textarea"]');
const findVisibilityRadioGroup = () =>
wrapper.find('[data-testid="fork-visibility-radio-group"]');
+ const findBranchesRadioGroup = () => wrapper.find('[data-testid="fork-branches-radio-group"]');
it('will go to cancelPath when click cancel button', () => {
createComponent();
@@ -203,11 +212,25 @@ describe('ForkForm component', () => {
});
});
+ describe('branches options', () => {
+ const formRadios = () => findBranchesRadioGroup().findAllComponents(GlFormRadio);
+ it('displays 2 branches options', () => {
+ createComponent();
+ expect(formRadios()).toHaveLength(2);
+ });
+
+ it('displays the correct description for each option', () => {
+ createComponent();
+ expect(formRadios().at(0).text()).toBe('All branches');
+ expect(formRadios().at(1).text()).toMatchInterpolatedText('Only the default branch main');
+ });
+ });
+
describe('visibility level', () => {
it('displays the correct description', () => {
createComponent();
- const formRadios = wrapper.findAllComponents(GlFormRadio);
+ const formRadios = findVisibilityRadioGroup().findAllComponents(GlFormRadio);
Object.keys(PROJECT_VISIBILITY_TYPE).forEach((visibilityType, index) => {
expect(formRadios.at(index).text()).toBe(PROJECT_VISIBILITY_TYPE[visibilityType]);
@@ -217,7 +240,7 @@ describe('ForkForm component', () => {
it('displays all 3 visibility levels', () => {
createComponent();
- expect(wrapper.findAllComponents(GlFormRadio)).toHaveLength(3);
+ expect(findVisibilityRadioGroup().findAllComponents(GlFormRadio)).toHaveLength(3);
});
describe('when the namespace is changed', () => {
@@ -236,7 +259,7 @@ describe('ForkForm component', () => {
it('resets the visibility to max allowed below current level', async () => {
createFullComponent({ projectVisibility: 'public' }, { namespaces });
- expect(findGlFormRadioGroup().vm.$attrs.checked).toBe('public');
+ expect(findVisibilityRadioGroup().vm.$attrs.checked).toBe('public');
fillForm({
name: 'one',
@@ -251,7 +274,7 @@ describe('ForkForm component', () => {
it('does not reset the visibility when current level is allowed', async () => {
createFullComponent({ projectVisibility: 'public' }, { namespaces });
- expect(findGlFormRadioGroup().vm.$attrs.checked).toBe('public');
+ expect(findVisibilityRadioGroup().vm.$attrs.checked).toBe('public');
fillForm({
name: 'two',
@@ -266,7 +289,7 @@ describe('ForkForm component', () => {
it('does not reset the visibility when visibility cap is increased', async () => {
createFullComponent({ projectVisibility: 'public' }, { namespaces });
- expect(findGlFormRadioGroup().vm.$attrs.checked).toBe('public');
+ expect(findVisibilityRadioGroup().vm.$attrs.checked).toBe('public');
fillForm({
name: 'three',
@@ -291,7 +314,7 @@ describe('ForkForm component', () => {
{ namespaces },
);
- await findGlFormRadioGroup().vm.$emit('input', 'internal');
+ await findVisibilityRadioGroup().vm.$emit('input', 'internal');
fillForm({
name: 'five',
id: 5,
@@ -469,7 +492,7 @@ describe('ForkForm component', () => {
jest.spyOn(axios, 'post');
setupComponent();
- await findGlFormRadioGroup().vm.$emit('input', null);
+ await findVisibilityRadioGroup().vm.$emit('input', null);
await nextTick();
@@ -533,6 +556,7 @@ describe('ForkForm component', () => {
const url = `/api/${GON_API_VERSION}/projects/${projectId}/fork`;
const project = {
+ branches: '',
description: projectDescription,
id: projectId,
name: projectName,
diff --git a/spec/frontend/pages/projects/pipeline_schedules/shared/components/interval_pattern_input_spec.js b/spec/frontend/pages/projects/pipeline_schedules/shared/components/interval_pattern_input_spec.js
index 50d09481b93..f6ecee4cd53 100644
--- a/spec/frontend/pages/projects/pipeline_schedules/shared/components/interval_pattern_input_spec.js
+++ b/spec/frontend/pages/projects/pipeline_schedules/shared/components/interval_pattern_input_spec.js
@@ -51,7 +51,7 @@ describe('Interval Pattern Input Component', () => {
beforeEach(() => {
oldWindowGl = window.gl;
window.gl = {
- ...(window.gl || {}),
+ ...window.gl,
pipelineScheduleFieldErrors: {
updateFormValidityState: jest.fn(),
},
diff --git a/spec/frontend/pages/registrations/new/index_spec.js b/spec/frontend/pages/registrations/new/index_spec.js
new file mode 100644
index 00000000000..4d30e5c9352
--- /dev/null
+++ b/spec/frontend/pages/registrations/new/index_spec.js
@@ -0,0 +1,10 @@
+import { renderGFM } from '~/behaviors/markdown/render_gfm';
+
+jest.mock('~/behaviors/markdown/render_gfm');
+
+describe('Sign up page', () => {
+ it('calls `renderGFM` to ensure that all gitlab-flavoured markdown is rendered on the sign up page', async () => {
+ await import('~/pages/sessions/new/index');
+ expect(renderGFM).toHaveBeenCalledWith(document.body);
+ });
+});
diff --git a/spec/frontend/pages/sessions/new/index_spec.js b/spec/frontend/pages/sessions/new/index_spec.js
new file mode 100644
index 00000000000..13aff16b3a9
--- /dev/null
+++ b/spec/frontend/pages/sessions/new/index_spec.js
@@ -0,0 +1,10 @@
+import { renderGFM } from '~/behaviors/markdown/render_gfm';
+
+jest.mock('~/behaviors/markdown/render_gfm');
+
+describe('Sign in page', () => {
+ it('calls `renderGFM` to ensure that all gitlab-flavoured markdown is rendered on the sign in page', async () => {
+ await import('~/pages/sessions/new/index');
+ expect(renderGFM).toHaveBeenCalledWith(document.body);
+ });
+});
diff --git a/spec/frontend/pipeline_wizard/templates/pages_spec.js b/spec/frontend/pipeline_wizard/templates/pages_spec.js
index f89e8f05475..72b3b1ca852 100644
--- a/spec/frontend/pipeline_wizard/templates/pages_spec.js
+++ b/spec/frontend/pipeline_wizard/templates/pages_spec.js
@@ -39,7 +39,11 @@ describe('Pages Template', () => {
}),
expect.objectContaining({
widget: 'checklist',
- title: 'Before we begin, please check:',
+ items: [
+ expect.objectContaining({
+ text: 'The application files are in the `public` folder',
+ }),
+ ],
}),
],
template: expect.stringContaining(VAR_BUILD_IMAGE),
diff --git a/spec/frontend/projects/members/utils_spec.js b/spec/frontend/projects/members/utils_spec.js
index 813e8455e85..2624851d9d8 100644
--- a/spec/frontend/projects/members/utils_spec.js
+++ b/spec/frontend/projects/members/utils_spec.js
@@ -8,7 +8,19 @@ describe('project member utils', () => {
accessLevel: 50,
expires_at: '2020-10-16',
}),
- ).toEqual({ project_member: { access_level: 50, expires_at: '2020-10-16' } });
+ ).toEqual({
+ project_member: { access_level: 50, expires_at: '2020-10-16', member_role_id: null },
+ });
+
+ expect(
+ projectMemberRequestFormatter({
+ accessLevel: 50,
+ expires_at: '2020-10-16',
+ memberRoleId: 80,
+ }),
+ ).toEqual({
+ project_member: { access_level: 50, expires_at: '2020-10-16', member_role_id: 80 },
+ });
});
});
});
diff --git a/spec/frontend/projects/pipelines/charts/components/__snapshots__/statistics_list_spec.js.snap b/spec/frontend/projects/pipelines/charts/components/__snapshots__/statistics_list_spec.js.snap
index 16d291804cc..a2877a9f17c 100644
--- a/spec/frontend/projects/pipelines/charts/components/__snapshots__/statistics_list_spec.js.snap
+++ b/spec/frontend/projects/pipelines/charts/components/__snapshots__/statistics_list_spec.js.snap
@@ -7,7 +7,7 @@ exports[`StatisticsList displays the counts data with labels 1`] = `
Total:
</span>
<strong>
- 4 pipelines
+ 40,000 pipelines
</strong>
</li>
<li>
@@ -15,7 +15,7 @@ exports[`StatisticsList displays the counts data with labels 1`] = `
Successful:
</span>
<strong>
- 2 pipelines
+ 20,000 pipelines
</strong>
</li>
<li>
@@ -25,7 +25,7 @@ exports[`StatisticsList displays the counts data with labels 1`] = `
<gl-link-stub
href="/flightjs/Flight/-/pipelines?page=1&scope=all&status=failed"
>
- 2 pipelines
+ 20,000 pipelines
</gl-link-stub>
</li>
<li>
diff --git a/spec/frontend/projects/pipelines/charts/mock_data.js b/spec/frontend/projects/pipelines/charts/mock_data.js
index 04971b5b20e..0e3e43835d0 100644
--- a/spec/frontend/projects/pipelines/charts/mock_data.js
+++ b/spec/frontend/projects/pipelines/charts/mock_data.js
@@ -1,7 +1,7 @@
export const counts = {
- failed: 2,
- success: 2,
- total: 4,
+ failed: 20000,
+ success: 20000,
+ total: 40000,
successRatio: 50,
totalDuration: 116158,
};
diff --git a/spec/frontend/projects/settings_service_desk/components/custom_email_form_spec.js b/spec/frontend/projects/settings_service_desk/components/custom_email_form_spec.js
index 9b012995ea4..efefcdb20df 100644
--- a/spec/frontend/projects/settings_service_desk/components/custom_email_form_spec.js
+++ b/spec/frontend/projects/settings_service_desk/components/custom_email_form_spec.js
@@ -1,11 +1,17 @@
import { mount } from '@vue/test-utils';
-import { GlLink } from '@gitlab/ui';
+import { GlFormSelect, GlLink } from '@gitlab/ui';
import { nextTick } from 'vue';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import { helpPagePath } from '~/helpers/help_page_helper';
import CustomEmailForm from '~/projects/settings_service_desk/components/custom_email_form.vue';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
-import { I18N_FORM_FORWARDING_CLIPBOARD_BUTTON_TITLE } from '~/projects/settings_service_desk/custom_email_constants';
+import {
+ I18N_FORM_FORWARDING_CLIPBOARD_BUTTON_TITLE,
+ I18N_FORM_SMTP_AUTHENTICATION_NONE,
+ I18N_FORM_SMTP_AUTHENTICATION_PLAIN,
+ I18N_FORM_SMTP_AUTHENTICATION_LOGIN,
+ I18N_FORM_SMTP_AUTHENTICATION_CRAM_MD5,
+} from '~/projects/settings_service_desk/custom_email_constants';
describe('CustomEmailForm', () => {
let wrapper;
@@ -24,6 +30,7 @@ describe('CustomEmailForm', () => {
const findSmtpPortInput = () => findInputByTestId('form-smtp-port');
const findSmtpUsernameInput = () => findInputByTestId('form-smtp-username');
const findSmtpPasswordInput = () => findInputByTestId('form-smtp-password');
+ const findSmtpAuthenticationSelect = () => wrapper.findComponent(GlFormSelect).find('select');
const findSubmit = () => wrapper.findByTestId('form-submit');
const clickButtonAndExpectNoSubmitEvent = async () => {
@@ -60,6 +67,23 @@ describe('CustomEmailForm', () => {
);
});
+ it('renders correct translations for options for SMTP authentication', () => {
+ createWrapper();
+
+ const translationStrings = [
+ I18N_FORM_SMTP_AUTHENTICATION_NONE,
+ I18N_FORM_SMTP_AUTHENTICATION_PLAIN,
+ I18N_FORM_SMTP_AUTHENTICATION_LOGIN,
+ I18N_FORM_SMTP_AUTHENTICATION_CRAM_MD5,
+ ];
+
+ findSmtpAuthenticationSelect()
+ .findAll('option')
+ .wrappers.forEach((item, index) => {
+ expect(item.text()).toEqual(translationStrings[index]);
+ });
+ });
+
it('form inputs are disabled when submitting', () => {
createWrapper({ isSubmitting: true });
@@ -68,6 +92,7 @@ describe('CustomEmailForm', () => {
expect(findSmtpPortInput().attributes('disabled')).toBeDefined();
expect(findSmtpUsernameInput().attributes('disabled')).toBeDefined();
expect(findSmtpPasswordInput().attributes('disabled')).toBeDefined();
+ expect(findSmtpAuthenticationSelect().attributes('disabled')).toBeDefined();
expect(findSubmit().props('loading')).toBe(true);
});
@@ -99,6 +124,8 @@ describe('CustomEmailForm', () => {
findSmtpPasswordInput().setValue('supersecret');
findSmtpPasswordInput().trigger('change');
+
+ findSmtpAuthenticationSelect().setValue('login');
});
it('is invalid when malformed email provided', async () => {
@@ -200,9 +227,10 @@ describe('CustomEmailForm', () => {
{
custom_email: 'user@example.com',
smtp_address: 'smtp.example.com',
+ smtp_username: 'user@example.com',
smtp_password: 'supersecret',
smtp_port: '587',
- smtp_username: 'user@example.com',
+ smtp_authentication: 'login',
},
],
]);
diff --git a/spec/frontend/projects/settings_service_desk/components/service_desk_root_spec.js b/spec/frontend/projects/settings_service_desk/components/service_desk_root_spec.js
index 8655845d1b7..0eec981b67d 100644
--- a/spec/frontend/projects/settings_service_desk/components/service_desk_root_spec.js
+++ b/spec/frontend/projects/settings_service_desk/components/service_desk_root_spec.js
@@ -22,6 +22,7 @@ describe('ServiceDeskRoot', () => {
isIssueTrackerEnabled: true,
outgoingName: 'GitLab Support Bot',
projectKey: 'key',
+ addExternalParticipantsFromCc: true,
selectedTemplate: 'Bug',
selectedFileTemplateProjectId: 42,
templates: ['Bug', 'Documentation'],
@@ -62,6 +63,7 @@ describe('ServiceDeskRoot', () => {
incomingEmail: provideData.initialIncomingEmail,
initialOutgoingName: provideData.outgoingName,
initialProjectKey: provideData.projectKey,
+ initialAddExternalParticipantsFromCc: provideData.addExternalParticipantsFromCc,
initialSelectedTemplate: provideData.selectedTemplate,
initialSelectedFileTemplateProjectId: provideData.selectedFileTemplateProjectId,
isEnabled: provideData.initialIsEnabled,
@@ -147,6 +149,7 @@ describe('ServiceDeskRoot', () => {
selectedTemplate: 'Bug',
outgoingName: 'GitLab Support Bot',
projectKey: 'key',
+ addExternalParticipantsFromCc: true,
};
wrapper.findComponent(ServiceDeskSetting).vm.$emit('save', payload);
@@ -160,6 +163,7 @@ describe('ServiceDeskRoot', () => {
outgoing_name: 'GitLab Support Bot',
project_key: 'key',
service_desk_enabled: true,
+ add_external_participants_from_cc: true,
});
});
@@ -178,6 +182,7 @@ describe('ServiceDeskRoot', () => {
selectedTemplate: 'Bug',
outgoingName: 'GitLab Support Bot',
projectKey: 'key',
+ addExternalParticipantsFromCc: true,
};
wrapper.findComponent(ServiceDeskSetting).vm.$emit('save', payload);
diff --git a/spec/frontend/projects/settings_service_desk/components/service_desk_setting_spec.js b/spec/frontend/projects/settings_service_desk/components/service_desk_setting_spec.js
index 260fd200f03..6449f9bb68e 100644
--- a/spec/frontend/projects/settings_service_desk/components/service_desk_setting_spec.js
+++ b/spec/frontend/projects/settings_service_desk/components/service_desk_setting_spec.js
@@ -1,4 +1,4 @@
-import { GlButton, GlDropdown, GlLoadingIcon, GlToggle, GlAlert } from '@gitlab/ui';
+import { GlButton, GlDropdown, GlFormCheckbox, GlLoadingIcon, GlToggle, GlAlert } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import { nextTick } from 'vue';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
@@ -19,8 +19,9 @@ describe('ServiceDeskSetting', () => {
const findSuffixFormGroup = () => wrapper.findByTestId('suffix-form-group');
const findIssueTrackerInfo = () => wrapper.findComponent(GlAlert);
const findIssueHelpLink = () => wrapper.findByTestId('issue-help-page');
+ const findAddExternalParticipantsFromCcCheckbox = () => wrapper.findComponent(GlFormCheckbox);
- const createComponent = ({ props = {} } = {}) =>
+ const createComponent = ({ props = {}, provide = {} } = {}) =>
extendedWrapper(
mount(ServiceDeskSetting, {
propsData: {
@@ -28,6 +29,12 @@ describe('ServiceDeskSetting', () => {
isIssueTrackerEnabled: true,
...props,
},
+ provide: {
+ glFeatures: {
+ issueEmailParticipants: true,
+ },
+ ...provide,
+ },
}),
);
@@ -205,6 +212,25 @@ describe('ServiceDeskSetting', () => {
});
});
+ describe('add external participants from cc checkbox', () => {
+ it('is rendered', () => {
+ wrapper = createComponent();
+ expect(findAddExternalParticipantsFromCcCheckbox().exists()).toBe(true);
+ });
+
+ it('forwards the initial value to the checkbox', () => {
+ wrapper = createComponent({ props: { initialAddExternalParticipantsFromCc: true } });
+ expect(findAddExternalParticipantsFromCcCheckbox().find('input').element.checked).toBe(true);
+ });
+
+ describe('when feature flag issue_email_participants is disabled', () => {
+ it('is not rendered', () => {
+ wrapper = createComponent({ provide: { glFeatures: { issueEmailParticipants: false } } });
+ expect(findAddExternalParticipantsFromCcCheckbox().exists()).toBe(false);
+ });
+ });
+ });
+
describe('save button', () => {
it('renders a save button to save a template', () => {
wrapper = createComponent();
@@ -223,6 +249,7 @@ describe('ServiceDeskSetting', () => {
initialSelectedFileTemplateProjectId: 42,
initialOutgoingName: 'GitLab Support Bot',
initialProjectKey: 'key',
+ initialAddExternalParticipantsFromCc: false,
},
});
@@ -235,6 +262,7 @@ describe('ServiceDeskSetting', () => {
fileTemplateProjectId: 42,
outgoingName: 'GitLab Support Bot',
projectKey: 'key',
+ addExternalParticipantsFromCc: false,
};
expect(wrapper.emitted('save')[0]).toEqual([payload]);
@@ -260,6 +288,10 @@ describe('ServiceDeskSetting', () => {
expect(findButton().exists()).toBe(false);
});
+ it('does not render add external participants from cc checkbox', () => {
+ expect(findAddExternalParticipantsFromCcCheckbox().exists()).toBe(false);
+ });
+
it('emits an event to turn on Service Desk when the toggle is clicked', async () => {
findToggle().vm.$emit('change', false);
diff --git a/spec/frontend/protected_branches/protected_branch_edit_spec.js b/spec/frontend/protected_branches/protected_branch_edit_spec.js
index 6422856ba22..301b0e8e157 100644
--- a/spec/frontend/protected_branches/protected_branch_edit_spec.js
+++ b/spec/frontend/protected_branches/protected_branch_edit_spec.js
@@ -6,37 +6,96 @@ import { createAlert } from '~/alert';
import axios from '~/lib/utils/axios_utils';
import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import ProtectedBranchEdit from '~/protected_branches/protected_branch_edit';
+import waitForPromises from 'helpers/wait_for_promises';
jest.mock('~/alert');
const TEST_URL = `${TEST_HOST}/url`;
+
+const response = {
+ project_id: 2,
+ name: 'release/*',
+ id: 30,
+ created_at: '2023-09-21T03:06:27.532Z',
+ updated_at: '2023-10-31T21:37:50.126Z',
+ code_owner_approval_required: false,
+ allow_force_push: false,
+ namespace_id: null,
+ merge_access_levels: [
+ {
+ id: 37,
+ protected_branch_id: 30,
+ access_level: 40,
+ created_at: '2023-10-31T22:44:15.545Z',
+ updated_at: '2023-10-31T22:44:15.545Z',
+ user_id: null,
+ group_id: null,
+ },
+ ],
+ push_access_levels: [
+ {
+ id: 38,
+ protected_branch_id: 30,
+ access_level: 40,
+ created_at: '2023-10-31T22:43:53.105Z',
+ updated_at: '2023-10-31T22:43:53.105Z',
+ user_id: null,
+ group_id: null,
+ deploy_key_id: null,
+ },
+ ],
+};
+
+// Toggles
const FORCE_PUSH_TOGGLE_TESTID = 'force-push-toggle';
const CODE_OWNER_TOGGLE_TESTID = 'code-owner-toggle';
const IS_CHECKED_CLASS = 'is-checked';
const IS_DISABLED_CLASS = 'is-disabled';
const IS_LOADING_SELECTOR = '.toggle-loading';
+// Dropdowns
+const MERGE_DROPDOWN_TESTID = 'protected-branch-allowed-to-merge';
+const PUSH_DROPDOWN_TESTID = 'protected-branch-allowed-to-push';
+const INIT_MERGE_DATA_TESTID = 'js-allowed-to-merge';
+const INIT_PUSH_DATA_TESTID = 'js-allowed-to-push';
+
describe('ProtectedBranchEdit', () => {
let mock;
- beforeEach(() => {
- jest.spyOn(ProtectedBranchEdit.prototype, 'initDropdowns').mockImplementation();
-
- mock = new MockAdapter(axios);
- });
-
const findForcePushToggle = () =>
document.querySelector(`div[data-testid="${FORCE_PUSH_TOGGLE_TESTID}"] button`);
const findCodeOwnerToggle = () =>
document.querySelector(`div[data-testid="${CODE_OWNER_TOGGLE_TESTID}"] button`);
+ const findMergeDropdown = () =>
+ document.querySelector(`div[data-testid="${MERGE_DROPDOWN_TESTID}"]`);
+ const findPushDropdown = () =>
+ document.querySelector(`div[data-testid="${PUSH_DROPDOWN_TESTID}"]`);
const create = ({
forcePushToggleChecked = false,
codeOwnerToggleChecked = false,
+ mergeClass = INIT_MERGE_DATA_TESTID,
+ mergeDisabled = false,
+ mergePreselected = [],
+ pushClass = INIT_PUSH_DATA_TESTID,
+ pushDisabled = false,
+ pushPreselected = [],
hasLicense = true,
} = {}) => {
setHTMLFixture(`<div id="wrap" data-url="${TEST_URL}">
<span
+ class="${mergeClass}"
+ data-label="Dropdown allowed to merge"
+ data-disabled="${mergeDisabled}"
+ data-preselected-items='${mergePreselected}'
+ data-testid="${MERGE_DROPDOWN_TESTID}"></span>
+ <span
+ class="${pushClass}"
+ data-label="Dropdown allowed to push"
+ data-disabled="${pushDisabled}"
+ data-preselected-items='${pushPreselected}'
+ data-testid="${PUSH_DROPDOWN_TESTID}"></span>
+ <span
class="js-force-push-toggle"
data-label="Toggle allowed to force push"
data-is-checked="${forcePushToggleChecked}"
@@ -51,108 +110,261 @@ describe('ProtectedBranchEdit', () => {
return new ProtectedBranchEdit({ $wrap: $('#wrap'), hasLicense });
};
+ beforeEach(() => {
+ mock = new MockAdapter(axios);
+ });
+
afterEach(() => {
mock.restore();
resetHTMLFixture();
});
- describe('when license supports code owner approvals', () => {
+ describe('dropdowns', () => {
+ const accessLevels = [
+ {
+ id: 40,
+ text: 'Maintainers',
+ before_divider: true,
+ },
+ {
+ id: 30,
+ text: 'Developers + Maintainers',
+ before_divider: true,
+ },
+ ];
+
beforeEach(() => {
- create();
- });
+ window.gon = {
+ current_project_id: 1,
+ merge_access_levels: { roles: accessLevels },
+ push_access_levels: { roles: accessLevels },
+ };
- it('instantiates the code owner toggle', () => {
- expect(findCodeOwnerToggle()).not.toBe(null);
+ jest.spyOn(ProtectedBranchEdit.prototype, 'initToggles').mockImplementation();
});
- });
- describe('when license does not support code owner approvals', () => {
- beforeEach(() => {
- create({ hasLicense: false });
- });
+ describe('rendering', () => {
+ describe('merge dropdown', () => {
+ it('builds the merge dropdown when it has the proper class', () => {
+ create();
+ expect(findMergeDropdown()).not.toBe(null);
+ });
- it('does not instantiate the code owner toggle', () => {
- expect(findCodeOwnerToggle()).toBe(null);
- });
- });
+ it('does not build the merge dropdown when it does not have the proper class', () => {
+ create({ mergeClass: 'invalid-class' });
+ expect(findMergeDropdown()).toBe(null);
+ });
+ });
- describe('when toggles are not available in the DOM on page load', () => {
- beforeEach(() => {
- create({ hasLicense: true });
- setHTMLFixture('');
- });
+ describe('push dropdown', () => {
+ it('builds the push dropdown when it has the proper class', () => {
+ create();
+ expect(findPushDropdown()).not.toBe(null);
+ });
- afterEach(() => {
- resetHTMLFixture();
+ it('does not build the push dropdown when it does not have the proper class', () => {
+ create({ pushClass: 'invalid-class' });
+ expect(findPushDropdown()).toBe(null);
+ });
+ });
});
- it('does not instantiate the force push toggle', () => {
- expect(findForcePushToggle()).toBe(null);
+ describe('preselected item', () => {
+ beforeEach(() => {
+ mock.onPatch(TEST_URL).reply(HTTP_STATUS_OK, response);
+ });
+
+ it('sets selected item on load', () => {
+ const preselected = [{ id: 38, access_level: 40, type: 'role' }];
+ const ProtectedBranchEditInstance = create({
+ pushPreselected: JSON.stringify(preselected),
+ });
+ const dropdown = ProtectedBranchEditInstance.push_access_levels_dropdown;
+ expect(dropdown.preselected).toEqual(preselected);
+ });
+
+ it('updates selected item on save for enabled dropdowns', async () => {
+ const selectedValue = [{ access_level: 40 }];
+ const ProtectedBranchEditInstance = create({});
+ const dropdown = ProtectedBranchEditInstance.push_access_levels_dropdown;
+ dropdown.$emit('select', selectedValue);
+ dropdown.$emit('hidden');
+ await waitForPromises();
+ expect(dropdown.preselected[0].id).toBe(response.push_access_levels[0].id);
+ });
+
+ it('does not update selected item on save for disabled dropdowns', async () => {
+ const selectedValue = [{ access_level: 40 }];
+ const ProtectedBranchEditInstance = create({ pushDisabled: '' });
+ const dropdown = ProtectedBranchEditInstance.push_access_levels_dropdown;
+ dropdown.$emit('select', selectedValue);
+ dropdown.$emit('hidden');
+ await waitForPromises();
+ expect(dropdown.preselected).toEqual([]);
+ });
});
- it('does not instantiate the code owner toggle', () => {
- expect(findCodeOwnerToggle()).toBe(null);
+ describe('on hidden', () => {
+ beforeEach(() => {
+ mock.onPatch(TEST_URL).reply(HTTP_STATUS_OK, {});
+ });
+
+ it('does not update permissions on hidden if there are no changes', () => {
+ const ProtectedBranchEditInstance = create();
+ const dropdown = ProtectedBranchEditInstance.merge_access_levels_dropdown;
+ dropdown.$emit('hidden');
+ expect(mock.history.patch).toHaveLength(0);
+ });
+
+ it('updates permissions on hidden for enabled dropdowns with changes', async () => {
+ const preselectedData = { id: 38, access_level: 40 };
+ const preselected = [{ ...preselectedData, type: 'role' }];
+ const selectedValue = [{ access_level: 30 }];
+ const ProtectedBranchEditInstance = create({
+ pushPreselected: JSON.stringify(preselected),
+ });
+ const dropdown = ProtectedBranchEditInstance.merge_access_levels_dropdown;
+ dropdown.$emit('select', selectedValue);
+ dropdown.$emit('hidden');
+ await waitForPromises();
+ expect(mock.history.patch).toHaveLength(1);
+ expect(mock.history.patch[0].data).toEqual(
+ JSON.stringify({
+ protected_branch: {
+ merge_access_levels_attributes: selectedValue,
+ push_access_levels_attributes: [preselectedData],
+ },
+ }),
+ );
+ });
+
+ it('does not update permissions on hidden for disabled dropdowns', async () => {
+ const preselected = [{ id: 38, access_level: 0, type: 'role' }];
+ const selectedValue = [{ access_level: 30 }];
+ const ProtectedBranchEditInstance = create({
+ mergeDisabled: '',
+ mergePreselected: JSON.stringify(preselected),
+ });
+ const dropdown = ProtectedBranchEditInstance.push_access_levels_dropdown;
+ dropdown.$emit('select', selectedValue);
+ dropdown.$emit('hidden');
+ await waitForPromises();
+ expect(mock.history.patch).toHaveLength(1);
+ expect(mock.history.patch[0].data).toEqual(
+ JSON.stringify({
+ protected_branch: {
+ merge_access_levels_attributes: [],
+ push_access_levels_attributes: selectedValue,
+ },
+ }),
+ );
+ });
});
});
- describe.each`
- description | checkedOption | patchParam | finder
- ${'force push'} | ${'forcePushToggleChecked'} | ${'allow_force_push'} | ${findForcePushToggle}
- ${'code owner'} | ${'codeOwnerToggleChecked'} | ${'code_owner_approval_required'} | ${findCodeOwnerToggle}
- `('when unchecked $description toggle button', ({ checkedOption, patchParam, finder }) => {
- let toggle;
-
+ describe('toggles', () => {
beforeEach(() => {
- create({ [checkedOption]: false });
+ jest.spyOn(ProtectedBranchEdit.prototype, 'initDropdowns').mockImplementation();
+ });
- toggle = finder();
+ describe('when license supports code owner approvals', () => {
+ beforeEach(() => {
+ create();
+ });
+
+ it('instantiates the code owner toggle', () => {
+ expect(findCodeOwnerToggle()).not.toBe(null);
+ });
});
- it('is not changed', () => {
- expect(toggle).not.toHaveClass(IS_CHECKED_CLASS);
- expect(toggle.querySelector(IS_LOADING_SELECTOR)).toBe(null);
- expect(toggle).not.toHaveClass(IS_DISABLED_CLASS);
+ describe('when license does not support code owner approvals', () => {
+ beforeEach(() => {
+ create({ hasLicense: false });
+ });
+
+ it('does not instantiate the code owner toggle', () => {
+ expect(findCodeOwnerToggle()).toBe(null);
+ });
});
- describe('when clicked', () => {
+ describe('when toggles are not available in the DOM on page load', () => {
beforeEach(() => {
- mock
- .onPatch(TEST_URL, { protected_branch: { [patchParam]: true } })
- .replyOnce(HTTP_STATUS_OK, {});
+ create({ hasLicense: true });
+ setHTMLFixture('');
});
- it('checks and disables button', async () => {
- await toggle.click();
+ afterEach(() => {
+ resetHTMLFixture();
+ });
- expect(toggle).toHaveClass(IS_CHECKED_CLASS);
- expect(toggle.querySelector(IS_LOADING_SELECTOR)).not.toBe(null);
- expect(toggle).toHaveClass(IS_DISABLED_CLASS);
+ it('does not instantiate the force push toggle', () => {
+ expect(findForcePushToggle()).toBe(null);
});
- it('sends update to BE', async () => {
- await toggle.click();
+ it('does not instantiate the code owner toggle', () => {
+ expect(findCodeOwnerToggle()).toBe(null);
+ });
+ });
- await axios.waitForAll();
+ describe.each`
+ description | checkedOption | patchParam | finder
+ ${'force push'} | ${'forcePushToggleChecked'} | ${'allow_force_push'} | ${findForcePushToggle}
+ ${'code owner'} | ${'codeOwnerToggleChecked'} | ${'code_owner_approval_required'} | ${findCodeOwnerToggle}
+ `('when unchecked $description toggle button', ({ checkedOption, patchParam, finder }) => {
+ let toggle;
- // Args are asserted in the `.onPatch` call
- expect(mock.history.patch).toHaveLength(1);
+ beforeEach(() => {
+ create({ [checkedOption]: false });
- expect(toggle).not.toHaveClass(IS_DISABLED_CLASS);
+ toggle = finder();
+ });
+
+ it('is not changed', () => {
+ expect(toggle).not.toHaveClass(IS_CHECKED_CLASS);
expect(toggle.querySelector(IS_LOADING_SELECTOR)).toBe(null);
- expect(createAlert).not.toHaveBeenCalled();
+ expect(toggle).not.toHaveClass(IS_DISABLED_CLASS);
});
- });
- describe('when clicked and BE error', () => {
- beforeEach(() => {
- mock.onPatch(TEST_URL).replyOnce(HTTP_STATUS_INTERNAL_SERVER_ERROR);
- toggle.click();
+ describe('when clicked', () => {
+ beforeEach(() => {
+ mock
+ .onPatch(TEST_URL, { protected_branch: { [patchParam]: true } })
+ .replyOnce(HTTP_STATUS_OK, {});
+ });
+
+ it('checks and disables button', async () => {
+ await toggle.click();
+
+ expect(toggle).toHaveClass(IS_CHECKED_CLASS);
+ expect(toggle.querySelector(IS_LOADING_SELECTOR)).not.toBe(null);
+ expect(toggle).toHaveClass(IS_DISABLED_CLASS);
+ });
+
+ it('sends update to BE', async () => {
+ await toggle.click();
+
+ await axios.waitForAll();
+
+ // Args are asserted in the `.onPatch` call
+ expect(mock.history.patch).toHaveLength(1);
+
+ expect(toggle).not.toHaveClass(IS_DISABLED_CLASS);
+ expect(toggle.querySelector(IS_LOADING_SELECTOR)).toBe(null);
+ expect(createAlert).not.toHaveBeenCalled();
+ });
});
- it('alerts error', async () => {
- await axios.waitForAll();
+ describe('when clicked and BE error', () => {
+ beforeEach(() => {
+ mock.onPatch(TEST_URL).replyOnce(HTTP_STATUS_INTERNAL_SERVER_ERROR);
+ toggle.click();
+ });
+
+ it('alerts error', async () => {
+ await axios.waitForAll();
- expect(createAlert).toHaveBeenCalled();
+ expect(createAlert).toHaveBeenCalled();
+ });
});
});
});
diff --git a/spec/frontend/repository/components/__snapshots__/last_commit_spec.js.snap b/spec/frontend/repository/components/__snapshots__/last_commit_spec.js.snap
index 1a5301c5525..99cfc4f418f 100644
--- a/spec/frontend/repository/components/__snapshots__/last_commit_spec.js.snap
+++ b/spec/frontend/repository/components/__snapshots__/last_commit_spec.js.snap
@@ -8,14 +8,12 @@ exports[`Repository last commit component renders commit widget 1`] = `
class="commit-actions gl-align-items-center gl-display-flex gl-flex-align gl-flex-direction-row"
>
<div
- class="ci-status-link"
+ class="gl-ml-5"
>
- <ci-badge-link-stub
+ <ci-icon-stub
aria-label="Pipeline: failed"
class="js-commit-pipeline"
- details-path="https://test.com/pipeline"
showtooltip="true"
- size="md"
status="[object Object]"
uselink="true"
/>
diff --git a/spec/frontend/repository/components/blob_content_viewer_spec.js b/spec/frontend/repository/components/blob_content_viewer_spec.js
index cc077e20e0b..e0d2984893b 100644
--- a/spec/frontend/repository/components/blob_content_viewer_spec.js
+++ b/spec/frontend/repository/components/blob_content_viewer_spec.js
@@ -18,6 +18,7 @@ import { loadViewer } from '~/repository/components/blob_viewers';
import DownloadViewer from '~/repository/components/blob_viewers/download_viewer.vue';
import EmptyViewer from '~/repository/components/blob_viewers/empty_viewer.vue';
import SourceViewer from '~/vue_shared/components/source_viewer/source_viewer.vue';
+import SourceViewerNew from '~/vue_shared/components/source_viewer/source_viewer_new.vue';
import blobInfoQuery from 'shared_queries/repository/blob_info.query.graphql';
import projectInfoQuery from '~/repository/queries/project_info.query.graphql';
import CodeIntelligence from '~/code_navigation/components/app.vue';
@@ -137,6 +138,7 @@ const createComponent = async (mockData = {}, mountFn = shallowMount, mockRoute
...inject,
glFeatures: {
highlightJsWorker: false,
+ blobBlameInfo: true,
},
},
}),
@@ -157,6 +159,7 @@ describe('Blob content viewer component', () => {
const findForkSuggestion = () => wrapper.findComponent(ForkSuggestion);
const findCodeIntelligence = () => wrapper.findComponent(CodeIntelligence);
const findSourceViewer = () => wrapper.findComponent(SourceViewer);
+ const findSourceViewerNew = () => wrapper.findComponent(SourceViewerNew);
beforeEach(() => {
jest.spyOn(window, 'requestIdleCallback').mockImplementation(execImmediately);
@@ -179,14 +182,43 @@ describe('Blob content viewer component', () => {
expect(findBlobHeader().props('activeViewerType')).toEqual(SIMPLE_BLOB_VIEWER);
expect(findBlobHeader().props('hasRenderError')).toEqual(false);
- expect(findBlobHeader().props('hideViewerSwitcher')).toEqual(true);
+ expect(findBlobHeader().props('hideViewerSwitcher')).toEqual(false);
expect(findBlobHeader().props('blob')).toEqual(simpleViewerMock);
expect(findBlobHeader().props('showForkSuggestion')).toEqual(false);
+ expect(findBlobHeader().props('showBlameToggle')).toEqual(false);
expect(findBlobHeader().props('projectPath')).toEqual(propsMock.projectPath);
expect(findBlobHeader().props('projectId')).toEqual(projectMock.id);
expect(mockRouterPush).not.toHaveBeenCalled();
});
+ describe('blame toggle', () => {
+ const triggerBlame = async () => {
+ findBlobHeader().vm.$emit('blame');
+ await nextTick();
+ };
+
+ it('renders a blame toggle for JSON files', async () => {
+ await createComponent({ blob: { ...simpleViewerMock, language: 'json' } });
+
+ expect(findBlobHeader().props('showBlameToggle')).toEqual(true);
+ });
+
+ it('adds blame param to the URL and passes `showBlame` to the SourceViewer', async () => {
+ loadViewer.mockReturnValueOnce(SourceViewerNew);
+ await createComponent({ blob: { ...simpleViewerMock, language: 'json' } });
+
+ await triggerBlame();
+
+ expect(mockRouterPush).toHaveBeenCalledWith({ query: { blame: '1' } });
+ expect(findSourceViewerNew().props('showBlame')).toBe(true);
+
+ await triggerBlame();
+
+ expect(mockRouterPush).toHaveBeenCalledWith({ query: { blame: '0' } });
+ expect(findSourceViewerNew().props('showBlame')).toBe(false);
+ });
+ });
+
it('creates an alert when the BlobHeader component emits an error', async () => {
await createComponent();
diff --git a/spec/frontend/repository/components/commit_info_spec.js b/spec/frontend/repository/components/commit_info_spec.js
index 34e941aa858..4e570346d97 100644
--- a/spec/frontend/repository/components/commit_info_spec.js
+++ b/spec/frontend/repository/components/commit_info_spec.js
@@ -21,9 +21,9 @@ const findAuthorName = () => wrapper.findByText(`${commit.authorName} authored`)
const findCommitRowDescription = () => wrapper.find('pre');
const findTitleHtml = () => wrapper.findByText(commit.titleHtml);
-const createComponent = async ({ commitMock = {} } = {}) => {
+const createComponent = async ({ commitMock = {}, prevBlameLink } = {}) => {
wrapper = shallowMountExtended(CommitInfo, {
- propsData: { commit: { ...commit, ...commitMock } },
+ propsData: { commit: { ...commit, ...commitMock }, prevBlameLink },
});
await nextTick();
@@ -35,6 +35,7 @@ describe('Repository last commit component', () => {
expect(findUserLink().exists()).toBe(true);
expect(findUserAvatarLink().exists()).toBe(true);
+ expect(findUserAvatarLink().props('imgAlt')).toBe("Test authorName's avatar");
});
it('hides author component when author does not exist', () => {
@@ -79,6 +80,22 @@ describe('Repository last commit component', () => {
});
});
+ describe('previous blame link', () => {
+ const prevBlameLink = '<a>Previous blame link</a>';
+
+ it('renders a previous blame link when it is present', () => {
+ createComponent({ prevBlameLink });
+
+ expect(wrapper.html()).toContain(prevBlameLink);
+ });
+
+ it('does not render a previous blame link when it is not present', () => {
+ createComponent({ prevBlameLink: null });
+
+ expect(wrapper.html()).not.toContain(prevBlameLink);
+ });
+ });
+
it('sets correct CSS class if the commit message is empty', () => {
createComponent({ commitMock: { message: '' } });
diff --git a/spec/frontend/repository/components/table/__snapshots__/row_spec.js.snap b/spec/frontend/repository/components/table/__snapshots__/row_spec.js.snap
index af7eca6a52d..e14f41e2ed2 100644
--- a/spec/frontend/repository/components/table/__snapshots__/row_spec.js.snap
+++ b/spec/frontend/repository/components/table/__snapshots__/row_spec.js.snap
@@ -34,15 +34,15 @@ exports[`Repository table row component renders a symlink table row 1`] = `
/>
</td>
<td
- class="cursor-default d-none d-sm-table-cell gl-text-secondary tree-commit"
+ class="cursor-default d-none d-sm-table-cell tree-commit"
>
<gl-link-stub
- class="gl-text-secondary str-truncated-100 tree-commit-link"
+ class="gl-text-gray-600 str-truncated-100 tree-commit-link"
/>
<gl-intersection-observer-stub />
</td>
<td
- class="cursor-default gl-text-secondary text-right tree-time-ago"
+ class="cursor-default gl-text-gray-600 text-right tree-time-ago"
>
<gl-intersection-observer-stub>
<timeago-tooltip-stub
@@ -90,15 +90,15 @@ exports[`Repository table row component renders table row 1`] = `
/>
</td>
<td
- class="cursor-default d-none d-sm-table-cell gl-text-secondary tree-commit"
+ class="cursor-default d-none d-sm-table-cell tree-commit"
>
<gl-link-stub
- class="gl-text-secondary str-truncated-100 tree-commit-link"
+ class="gl-text-gray-600 str-truncated-100 tree-commit-link"
/>
<gl-intersection-observer-stub />
</td>
<td
- class="cursor-default gl-text-secondary text-right tree-time-ago"
+ class="cursor-default gl-text-gray-600 text-right tree-time-ago"
>
<gl-intersection-observer-stub>
<timeago-tooltip-stub
@@ -146,15 +146,15 @@ exports[`Repository table row component renders table row for path with special
/>
</td>
<td
- class="cursor-default d-none d-sm-table-cell gl-text-secondary tree-commit"
+ class="cursor-default d-none d-sm-table-cell tree-commit"
>
<gl-link-stub
- class="gl-text-secondary str-truncated-100 tree-commit-link"
+ class="gl-text-gray-600 str-truncated-100 tree-commit-link"
/>
<gl-intersection-observer-stub />
</td>
<td
- class="cursor-default gl-text-secondary text-right tree-time-ago"
+ class="cursor-default gl-text-gray-600 text-right tree-time-ago"
>
<gl-intersection-observer-stub>
<timeago-tooltip-stub
diff --git a/spec/frontend/search/sidebar/components/app_spec.js b/spec/frontend/search/sidebar/components/app_spec.js
index d8d2492209e..c2d88493d71 100644
--- a/spec/frontend/search/sidebar/components/app_spec.js
+++ b/spec/frontend/search/sidebar/components/app_spec.js
@@ -17,6 +17,7 @@ import ProjectsFilters from '~/search/sidebar/components/projects_filters.vue';
import NotesFilters from '~/search/sidebar/components/notes_filters.vue';
import CommitsFilters from '~/search/sidebar/components/commits_filters.vue';
import MilestonesFilters from '~/search/sidebar/components/milestones_filters.vue';
+import WikiBlobsFilters from '~/search/sidebar/components/wiki_blobs_filters.vue';
import ScopeLegacyNavigation from '~/search/sidebar/components/scope_legacy_navigation.vue';
import SmallScreenDrawerNavigation from '~/search/sidebar/components/small_screen_drawer_navigation.vue';
import ScopeSidebarNavigation from '~/search/sidebar/components/scope_sidebar_navigation.vue';
@@ -46,9 +47,7 @@ describe('GlobalSearchSidebar', () => {
store,
provide: {
glFeatures: {
- searchNotesHideArchivedProjects: true,
- searchCommitsHideArchivedProjects: true,
- searchMilestonesHideArchivedProjects: true,
+ searchProjectWikisHideArchivedProjects: true,
},
},
});
@@ -62,6 +61,7 @@ describe('GlobalSearchSidebar', () => {
const findNotesFilters = () => wrapper.findComponent(NotesFilters);
const findCommitsFilters = () => wrapper.findComponent(CommitsFilters);
const findMilestonesFilters = () => wrapper.findComponent(MilestonesFilters);
+ const findWikiBlobsFilters = () => wrapper.findComponent(WikiBlobsFilters);
const findScopeLegacyNavigation = () => wrapper.findComponent(ScopeLegacyNavigation);
const findSmallScreenDrawerNavigation = () => wrapper.findComponent(SmallScreenDrawerNavigation);
const findScopeSidebarNavigation = () => wrapper.findComponent(ScopeSidebarNavigation);
@@ -92,6 +92,8 @@ describe('GlobalSearchSidebar', () => {
${'commits'} | ${findCommitsFilters} | ${SEARCH_TYPE_ADVANCED} | ${true}
${'milestones'} | ${findMilestonesFilters} | ${SEARCH_TYPE_BASIC} | ${true}
${'milestones'} | ${findMilestonesFilters} | ${SEARCH_TYPE_ADVANCED} | ${true}
+ ${'wiki_blobs'} | ${findWikiBlobsFilters} | ${SEARCH_TYPE_BASIC} | ${true}
+ ${'wiki_blobs'} | ${findWikiBlobsFilters} | ${SEARCH_TYPE_ADVANCED} | ${true}
`('with sidebar $scope scope:', ({ scope, filter, searchType, isShown }) => {
beforeEach(() => {
getterSpies.currentScope = jest.fn(() => scope);
diff --git a/spec/frontend/search/sidebar/components/blobs_filters_spec.js b/spec/frontend/search/sidebar/components/blobs_filters_spec.js
index 729fae44c19..245ddb8f8bb 100644
--- a/spec/frontend/search/sidebar/components/blobs_filters_spec.js
+++ b/spec/frontend/search/sidebar/components/blobs_filters_spec.js
@@ -17,7 +17,7 @@ describe('GlobalSearch BlobsFilters', () => {
currentScope: () => 'blobs',
};
- const createComponent = ({ initialState = {}, searchBlobsHideArchivedProjects = true } = {}) => {
+ const createComponent = ({ initialState = {} } = {}) => {
const store = new Vuex.Store({
state: {
urlQuery: MOCK_QUERY,
@@ -30,11 +30,6 @@ describe('GlobalSearch BlobsFilters', () => {
wrapper = shallowMount(BlobsFilters, {
store,
- provide: {
- glFeatures: {
- searchBlobsHideArchivedProjects,
- },
- },
});
};
@@ -42,29 +37,20 @@ describe('GlobalSearch BlobsFilters', () => {
const findArchivedFilter = () => wrapper.findComponent(ArchivedFilter);
const findDividers = () => wrapper.findAll('hr');
- describe.each`
- description | searchBlobsHideArchivedProjects
- ${'Renders correctly with Archived Filter enabled'} | ${true}
- ${'Renders correctly with Archived Filter disabled'} | ${false}
- `('$description', ({ searchBlobsHideArchivedProjects }) => {
- beforeEach(() => {
- createComponent({
- searchBlobsHideArchivedProjects,
- });
- });
+ beforeEach(() => {
+ createComponent({});
+ });
- it('renders LanguageFilter', () => {
- expect(findLanguageFilter().exists()).toBe(true);
- });
+ it('renders LanguageFilter', () => {
+ expect(findLanguageFilter().exists()).toBe(true);
+ });
- it(`renders correctly ArchivedFilter when searchBlobsHideArchivedProjects is ${searchBlobsHideArchivedProjects}`, () => {
- expect(findArchivedFilter().exists()).toBe(searchBlobsHideArchivedProjects);
- });
+ it('renders ArchivedFilter', () => {
+ expect(findArchivedFilter().exists()).toBe(true);
+ });
- it('renders divider correctly', () => {
- const dividersCount = searchBlobsHideArchivedProjects ? 1 : 0;
- expect(findDividers()).toHaveLength(dividersCount);
- });
+ it('renders divider correctly', () => {
+ expect(findDividers()).toHaveLength(1);
});
describe('Renders correctly in new nav', () => {
@@ -74,7 +60,6 @@ describe('GlobalSearch BlobsFilters', () => {
searchType: SEARCH_TYPE_ADVANCED,
useSidebarNavigation: true,
},
- searchBlobsHideArchivedProjects: true,
});
});
diff --git a/spec/frontend/search/sidebar/components/issues_filters_spec.js b/spec/frontend/search/sidebar/components/issues_filters_spec.js
index c3b3a93e362..860c5c147a6 100644
--- a/spec/frontend/search/sidebar/components/issues_filters_spec.js
+++ b/spec/frontend/search/sidebar/components/issues_filters_spec.js
@@ -19,11 +19,7 @@ describe('GlobalSearch IssuesFilters', () => {
currentScope: () => 'issues',
};
- const createComponent = ({
- initialState = {},
- searchIssueLabelAggregation = true,
- searchIssuesHideArchivedProjects = true,
- } = {}) => {
+ const createComponent = ({ initialState = {}, searchIssueLabelAggregation = true } = {}) => {
const store = new Vuex.Store({
state: {
urlQuery: MOCK_QUERY,
@@ -39,7 +35,6 @@ describe('GlobalSearch IssuesFilters', () => {
provide: {
glFeatures: {
searchIssueLabelAggregation,
- searchIssuesHideArchivedProjects,
},
},
});
@@ -52,16 +47,13 @@ describe('GlobalSearch IssuesFilters', () => {
const findDividers = () => wrapper.findAll('hr');
describe.each`
- description | searchIssueLabelAggregation | searchIssuesHideArchivedProjects
- ${'Renders correctly with Label Filter disabled'} | ${false} | ${true}
- ${'Renders correctly with Archived Filter disabled'} | ${true} | ${false}
- ${'Renders correctly with Archived Filter and Label Filter disabled'} | ${false} | ${false}
- ${'Renders correctly with Archived Filter and Label Filter enabled'} | ${true} | ${true}
- `('$description', ({ searchIssueLabelAggregation, searchIssuesHideArchivedProjects }) => {
+ description | searchIssueLabelAggregation
+ ${'Renders correctly with Label Filter disabled'} | ${false}
+ ${'Renders correctly with Label Filter enabled'} | ${true}
+ `('$description', ({ searchIssueLabelAggregation }) => {
beforeEach(() => {
createComponent({
searchIssueLabelAggregation,
- searchIssuesHideArchivedProjects,
});
});
@@ -73,23 +65,20 @@ describe('GlobalSearch IssuesFilters', () => {
expect(findConfidentialityFilter().exists()).toBe(true);
});
- it(`renders correctly LabelFilter when searchIssueLabelAggregation is ${searchIssueLabelAggregation}`, () => {
- expect(findLabelFilter().exists()).toBe(searchIssueLabelAggregation);
+ it('renders correctly ArchivedFilter', () => {
+ expect(findArchivedFilter().exists()).toBe(true);
});
- it(`renders correctly ArchivedFilter when searchIssuesHideArchivedProjects is ${searchIssuesHideArchivedProjects}`, () => {
- expect(findArchivedFilter().exists()).toBe(searchIssuesHideArchivedProjects);
+ it(`renders correctly LabelFilter when searchIssueLabelAggregation is ${searchIssueLabelAggregation}`, () => {
+ expect(findLabelFilter().exists()).toBe(searchIssueLabelAggregation);
});
it('renders divider correctly', () => {
- // one divider can't be disabled
- let dividersCount = 1;
+ // two dividers can't be disabled
+ let dividersCount = 2;
if (searchIssueLabelAggregation) {
dividersCount += 1;
}
- if (searchIssuesHideArchivedProjects) {
- dividersCount += 1;
- }
expect(findDividers()).toHaveLength(dividersCount);
});
});
@@ -127,7 +116,6 @@ describe('GlobalSearch IssuesFilters', () => {
useSidebarNavigation: true,
},
searchIssueLabelAggregation: true,
- searchIssuesHideArchivedProjects: true,
});
});
it('renders StatusFilter', () => {
diff --git a/spec/frontend/search/sidebar/components/label_filter_spec.js b/spec/frontend/search/sidebar/components/label_filter_spec.js
index 07b2e176610..9d2a0c5e739 100644
--- a/spec/frontend/search/sidebar/components/label_filter_spec.js
+++ b/spec/frontend/search/sidebar/components/label_filter_spec.js
@@ -13,7 +13,11 @@ import Vue from 'vue';
import Vuex from 'vuex';
import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
import { mountExtended } from 'helpers/vue_test_utils_helper';
-import { MOCK_QUERY, MOCK_LABEL_AGGREGATIONS } from 'jest/search/mock_data';
+import {
+ MOCK_QUERY,
+ MOCK_LABEL_AGGREGATIONS,
+ MOCK_FILTERED_UNSELECTED_LABELS,
+} from 'jest/search/mock_data';
import LabelFilter from '~/search/sidebar/components/label_filter/index.vue';
import LabelDropdownItems from '~/search/sidebar/components/label_filter/label_dropdown_items.vue';
@@ -52,8 +56,15 @@ describe('GlobalSearchSidebarLabelFilter', () => {
let trackingSpy;
let config;
let store;
+ let state;
+
+ const createComponent = (initialState, gettersStubs) => {
+ state = createState({
+ query: MOCK_QUERY,
+ aggregations: MOCK_LABEL_AGGREGATIONS,
+ ...initialState,
+ });
- const createComponent = (initialState) => {
config = {
actions: {
...actions,
@@ -62,13 +73,12 @@ describe('GlobalSearchSidebarLabelFilter', () => {
setLabelFilterSearch: actionSpies.setLabelFilterSearch,
setQuery: actionSpies.setQuery,
},
- getters,
+ state,
+ getters: {
+ ...getters,
+ ...gettersStubs,
+ },
mutations,
- state: createState({
- query: MOCK_QUERY,
- aggregations: MOCK_LABEL_AGGREGATIONS,
- ...initialState,
- }),
};
store = new Vuex.Store(config);
@@ -95,6 +105,10 @@ describe('GlobalSearchSidebarLabelFilter', () => {
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const findNoLabelsFoundMessage = () => wrapper.findComponentByTestId('no-labels-found-message');
+ const findLabelPills = () => wrapper.findAllComponentsByTestId('label');
+ const findSelectedUappliedLavelPills = () => wrapper.findAllComponentsByTestId('unapplied-label');
+ const findClosedUnappliedPills = () => wrapper.findAllComponentsByTestId('unselected-label');
+
describe('Renders correctly closed', () => {
beforeEach(async () => {
createComponent();
@@ -349,5 +363,42 @@ describe('GlobalSearchSidebarLabelFilter', () => {
});
});
});
+
+ describe('newly selected and unapplied labels show as pills above dropdown', () => {
+ beforeEach(() => {
+ const mockGetters = { unappliedNewLabels: jest.fn(() => MOCK_FILTERED_UNSELECTED_LABELS) };
+ createComponent({}, mockGetters);
+ });
+
+ it('has correct pills', () => {
+ expect(findSelectedUappliedLavelPills()).toHaveLength(2);
+ });
+ });
+
+ describe('applied labels show as pills above dropdown', () => {
+ beforeEach(() => {
+ const mockGetters = {
+ appliedSelectedLabels: jest.fn(() => MOCK_FILTERED_UNSELECTED_LABELS),
+ };
+ createComponent({}, mockGetters);
+ });
+
+ it('has correct pills', () => {
+ expect(findLabelPills()).toHaveLength(2);
+ });
+ });
+
+ describe('closed unapplied labels show as pills above dropdown', () => {
+ beforeEach(() => {
+ const mockGetters = {
+ unselectedLabels: jest.fn(() => MOCK_FILTERED_UNSELECTED_LABELS),
+ };
+ createComponent({}, mockGetters);
+ });
+
+ it('has correct pills', () => {
+ expect(findClosedUnappliedPills()).toHaveLength(2);
+ });
+ });
});
});
diff --git a/spec/frontend/search/sidebar/components/merge_requests_filters_spec.js b/spec/frontend/search/sidebar/components/merge_requests_filters_spec.js
index 278249c2660..b02228a418f 100644
--- a/spec/frontend/search/sidebar/components/merge_requests_filters_spec.js
+++ b/spec/frontend/search/sidebar/components/merge_requests_filters_spec.js
@@ -17,10 +17,7 @@ describe('GlobalSearch MergeRequestsFilters', () => {
currentScope: () => 'merge_requests',
};
- const createComponent = ({
- initialState = {},
- searchMergeRequestsHideArchivedProjects = true,
- } = {}) => {
+ const createComponent = (initialState = {}) => {
const store = new Vuex.Store({
state: {
urlQuery: MOCK_QUERY,
@@ -33,11 +30,6 @@ describe('GlobalSearch MergeRequestsFilters', () => {
wrapper = shallowMount(MergeRequestsFilters, {
store,
- provide: {
- glFeatures: {
- searchMergeRequestsHideArchivedProjects,
- },
- },
});
};
@@ -45,34 +37,23 @@ describe('GlobalSearch MergeRequestsFilters', () => {
const findArchivedFilter = () => wrapper.findComponent(ArchivedFilter);
const findDividers = () => wrapper.findAll('hr');
- describe.each`
- description | searchMergeRequestsHideArchivedProjects
- ${'Renders correctly with Archived Filter disabled'} | ${false}
- ${'Renders correctly with Archived Filter enabled'} | ${true}
- `('$description', ({ searchMergeRequestsHideArchivedProjects }) => {
+ describe('Renders correctly with Archived Filter', () => {
beforeEach(() => {
- createComponent({
- searchMergeRequestsHideArchivedProjects,
- });
+ createComponent();
});
it('renders StatusFilter', () => {
expect(findStatusFilter().exists()).toBe(true);
});
- it(`renders correctly ArchivedFilter when searchMergeRequestsHideArchivedProjects is ${searchMergeRequestsHideArchivedProjects}`, () => {
- expect(findArchivedFilter().exists()).toBe(searchMergeRequestsHideArchivedProjects);
- });
-
it('renders divider correctly', () => {
- const dividersCount = searchMergeRequestsHideArchivedProjects ? 1 : 0;
- expect(findDividers()).toHaveLength(dividersCount);
+ expect(findDividers()).toHaveLength(1);
});
});
describe('Renders correctly with basic search', () => {
beforeEach(() => {
- createComponent({ initialState: { searchType: SEARCH_TYPE_BASIC } });
+ createComponent({ searchType: SEARCH_TYPE_BASIC });
});
it('renders StatusFilter', () => {
@@ -91,11 +72,8 @@ describe('GlobalSearch MergeRequestsFilters', () => {
describe('Renders correctly in new nav', () => {
beforeEach(() => {
createComponent({
- initialState: {
- searchType: SEARCH_TYPE_ADVANCED,
- useSidebarNavigation: true,
- },
- searchMergeRequestsHideArchivedProjects: true,
+ searchType: SEARCH_TYPE_ADVANCED,
+ useSidebarNavigation: true,
});
});
it('renders StatusFilter', () => {
diff --git a/spec/frontend/search/store/getters_spec.js b/spec/frontend/search/store/getters_spec.js
index 571525bd025..8e988ce5c4a 100644
--- a/spec/frontend/search/store/getters_spec.js
+++ b/spec/frontend/search/store/getters_spec.js
@@ -134,4 +134,23 @@ describe('Global Search Store Getters', () => {
]);
});
});
+
+ describe('unselectedLabels', () => {
+ it('returns all labels that are not selected', () => {
+ state.query.labels = ['60'];
+ expect(getters.unselectedLabels(state)).toStrictEqual([MOCK_LABEL_SEARCH_RESULT]);
+ });
+ });
+
+ describe('unappliedNewLabels', () => {
+ it('returns all labels that are selected but not applied', () => {
+ // Applied labels
+ state.urlQuery.labels = ['37', '60'];
+ // Applied and selected labels
+ state.query.labels = ['37', '6', '73', '60'];
+ // Selected but unapplied labels
+ // expect(getters.unappliedNewLabels(state)).toStrictEqual(MOCK_FILTERED_UNSELECTED_LABELS);
+ expect(getters.unappliedNewLabels(state).map(({ key }) => key)).toStrictEqual(['6', '73']);
+ });
+ });
});
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 2982cef7c74..5b2b3f46df6 100644
--- a/spec/frontend/security_configuration/components/training_provider_list_spec.js
+++ b/spec/frontend/security_configuration/components/training_provider_list_spec.js
@@ -1,4 +1,3 @@
-import * as Sentry from '@sentry/browser';
import {
GlAlert,
GlLink,
@@ -10,6 +9,7 @@ import {
} from '@gitlab/ui';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
diff --git a/spec/frontend/sentry/init_sentry_spec.js b/spec/frontend/sentry/init_sentry_spec.js
index fb0dba35759..118a48cc1de 100644
--- a/spec/frontend/sentry/init_sentry_spec.js
+++ b/spec/frontend/sentry/init_sentry_spec.js
@@ -87,7 +87,7 @@ describe('SentryConfig', () => {
expect(mockBrowserClient).toHaveBeenCalledWith(
expect.objectContaining({
dsn: mockDsn,
- release: mockVersion,
+ release: mockRevision,
allowUrls: [mockGitlabUrl, 'webpack-internal://'],
environment: mockEnvironment,
tracesSampleRate: mockSentryClientsideTracesSampleRate,
@@ -115,7 +115,7 @@ describe('SentryConfig', () => {
expect(mockSetTags).toHaveBeenCalledTimes(1);
expect(mockSetTags).toHaveBeenCalledWith({
page: mockPage,
- revision: mockRevision,
+ version: mockVersion,
feature_category: mockFeatureCategory,
});
});
diff --git a/spec/frontend/sentry/sentry_browser_wrapper_spec.js b/spec/frontend/sentry/sentry_browser_wrapper_spec.js
index d98286e1371..60c441fe83c 100644
--- a/spec/frontend/sentry/sentry_browser_wrapper_spec.js
+++ b/spec/frontend/sentry/sentry_browser_wrapper_spec.js
@@ -1,18 +1,39 @@
+/* eslint-disable no-console */
+
import * as Sentry from '~/sentry/sentry_browser_wrapper';
const mockError = new Error('error!');
describe('SentryBrowserWrapper', () => {
+ beforeAll(() => {
+ process.env.NODE_ENV = 'development';
+ });
+
+ afterAll(() => {
+ process.env.NODE_ENV = 'test';
+ });
+
+ beforeEach(() => {
+ jest.spyOn(console, 'error').mockImplementation();
+ });
+
afterEach(() => {
+ console.error.mockRestore();
+
// eslint-disable-next-line no-underscore-dangle
delete window._Sentry;
});
describe('when _Sentry is not defined', () => {
- it('methods fail silently', () => {
- expect(() => {
- Sentry.captureException(mockError);
- }).not.toThrow();
+ it('captureException will report to console instead', () => {
+ Sentry.captureException(mockError);
+
+ expect(console.error).toHaveBeenCalledTimes(1);
+ expect(console.error).toHaveBeenCalledWith(
+ '[Sentry stub]',
+ 'captureException(...) called with:',
+ { 0: mockError },
+ );
});
});
diff --git a/spec/frontend/shortcuts_spec.js b/spec/frontend/shortcuts_spec.js
index 88ad9204d08..ca72426cb44 100644
--- a/spec/frontend/shortcuts_spec.js
+++ b/spec/frontend/shortcuts_spec.js
@@ -7,23 +7,26 @@ import Shortcuts, { LOCAL_MOUSETRAP_DATA_KEY } from '~/behaviors/shortcuts/short
import MarkdownPreview from '~/behaviors/preview_markdown';
describe('Shortcuts', () => {
- const createEvent = (type, target) =>
- $.Event(type, {
- target,
- });
let shortcuts;
beforeAll(() => {
shortcuts = new Shortcuts();
});
+ const mockSuperSidebarSearchButton = () => {
+ const button = document.createElement('button');
+ button.id = 'super-sidebar-search';
+ return button;
+ };
+
beforeEach(() => {
setHTMLFixture(htmlSnippetsShow);
+ document.body.appendChild(mockSuperSidebarSearchButton());
new Shortcuts(); // eslint-disable-line no-new
new MarkdownPreview(); // eslint-disable-line no-new
- jest.spyOn(document.querySelector('#search'), 'focus');
+ jest.spyOn(HTMLElement.prototype, 'click');
jest.spyOn(Mousetrap.prototype, 'stopCallback');
jest.spyOn(Mousetrap.prototype, 'bind').mockImplementation();
@@ -100,21 +103,22 @@ describe('Shortcuts', () => {
});
describe('focusSearch', () => {
- describe('when super sidebar is NOT enabled', () => {
- let originalGon;
- beforeEach(() => {
- originalGon = window.gon;
- window.gon = { use_new_navigation: false };
- });
+ let event;
- afterEach(() => {
- window.gon = originalGon;
- });
+ beforeEach(() => {
+ window.gon.use_new_navigation = true;
+ event = new KeyboardEvent('keydown', { cancelable: true });
+ Shortcuts.focusSearch(event);
+ });
- it('focuses the search bar', () => {
- Shortcuts.focusSearch(createEvent('KeyboardEvent'));
- expect(document.querySelector('#search').focus).toHaveBeenCalled();
- });
+ it('clicks the super sidebar search button', () => {
+ expect(HTMLElement.prototype.click).toHaveBeenCalled();
+ const thisArg = HTMLElement.prototype.click.mock.contexts[0];
+ expect(thisArg.id).toBe('super-sidebar-search');
+ });
+
+ it('cancels the default behaviour of the event', () => {
+ expect(event.defaultPrevented).toBe(true);
});
});
diff --git a/spec/frontend/sidebar/components/lock/__snapshots__/edit_form_spec.js.snap b/spec/frontend/sidebar/components/lock/__snapshots__/edit_form_spec.js.snap
index d5bbd3bb3c9..48ba23ac0a1 100644
--- a/spec/frontend/sidebar/components/lock/__snapshots__/edit_form_spec.js.snap
+++ b/spec/frontend/sidebar/components/lock/__snapshots__/edit_form_spec.js.snap
@@ -9,7 +9,7 @@ exports[`Edit Form Dropdown In issue page when locked the appropriate warning te
class="text"
>
<gl-sprintf-stub
- message="Unlock this %{issuableDisplayName}? %{strongStart}Everyone%{strongEnd} will be able to comment."
+ message="Unlock this discussion? %{strongStart}Everyone%{strongEnd} will be able to comment."
/>
</p>
<edit-form-buttons-stub
@@ -28,7 +28,7 @@ exports[`Edit Form Dropdown In issue page when unlocked the appropriate warning
class="text"
>
<gl-sprintf-stub
- message="Lock this %{issuableDisplayName}? Only %{strongStart}project members%{strongEnd} will be able to comment."
+ message="Lock this discussion? Only %{strongStart}project members%{strongEnd} will be able to comment."
/>
</p>
<edit-form-buttons-stub
@@ -46,7 +46,7 @@ exports[`Edit Form Dropdown In merge request page when locked the appropriate wa
class="text"
>
<gl-sprintf-stub
- message="Unlock this %{issuableDisplayName}? %{strongStart}Everyone%{strongEnd} will be able to comment."
+ message="Unlock this discussion? %{strongStart}Everyone%{strongEnd} will be able to comment."
/>
</p>
<edit-form-buttons-stub
@@ -65,7 +65,7 @@ exports[`Edit Form Dropdown In merge request page when unlocked the appropriate
class="text"
>
<gl-sprintf-stub
- message="Lock this %{issuableDisplayName}? Only %{strongStart}project members%{strongEnd} will be able to comment."
+ message="Lock this discussion? Only %{strongStart}project members%{strongEnd} will be able to comment."
/>
</p>
<edit-form-buttons-stub
diff --git a/spec/frontend/sidebar/components/lock/issuable_lock_form_spec.js b/spec/frontend/sidebar/components/lock/issuable_lock_form_spec.js
index e1c41fb8b46..69531af6e3a 100644
--- a/spec/frontend/sidebar/components/lock/issuable_lock_form_spec.js
+++ b/spec/frontend/sidebar/components/lock/issuable_lock_form_spec.js
@@ -176,8 +176,8 @@ describe('IssuableLockForm', () => {
it.each`
locked | message
- ${true} | ${'Merge request locked.'}
- ${false} | ${'Merge request unlocked.'}
+ ${true} | ${'Discussion locked.'}
+ ${false} | ${'Discussion unlocked.'}
`('displays $message when merge request is $locked', async ({ locked, message }) => {
initStore(locked);
diff --git a/spec/frontend/sidebar/components/move/issuable_move_dropdown_spec.js b/spec/frontend/sidebar/components/move/issuable_move_dropdown_spec.js
index 56c915c4cae..f049001ba45 100644
--- a/spec/frontend/sidebar/components/move/issuable_move_dropdown_spec.js
+++ b/spec/frontend/sidebar/components/move/issuable_move_dropdown_spec.js
@@ -1,19 +1,9 @@
-import { nextTick } from 'vue';
-import {
- GlIcon,
- GlLoadingIcon,
- GlDropdown,
- GlDropdownForm,
- GlDropdownItem,
- GlSearchBoxByType,
- GlButton,
-} from '@gitlab/ui';
+import { GlCollapsibleListbox, GlListboxItem } from '@gitlab/ui';
import MockAdapter from 'axios-mock-adapter';
import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
import axios from '~/lib/utils/axios_utils';
import IssuableMoveDropdown from '~/sidebar/components/move/issuable_move_dropdown.vue';
-import { stubComponent } from 'helpers/stub_component';
-import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
const mockProjects = [
@@ -42,318 +32,136 @@ const mockProps = {
disabled: false,
};
-const mockEvent = {
- stopPropagation: jest.fn(),
- preventDefault: jest.fn(),
-};
-
-const focusInputMock = jest.fn();
-const hideMock = jest.fn();
-
describe('IssuableMoveDropdown', () => {
let mock;
let wrapper;
- const createComponent = (propsData = mockProps) => {
- wrapper = shallowMountExtended(IssuableMoveDropdown, {
- propsData,
- stubs: {
- GlDropdown: stubComponent(GlDropdown, {
- methods: {
- hide: hideMock,
- },
- }),
- GlSearchBoxByType: stubComponent(GlSearchBoxByType, {
- methods: {
- focusInput: focusInputMock,
- },
- }),
- },
- });
+ const createComponent = (propsData = {}) => {
+ wrapper = mountExtended(IssuableMoveDropdown, { propsData: { ...mockProps, ...propsData } });
};
beforeEach(() => {
mock = new MockAdapter(axios);
mock.onGet(mockProps.projectsFetchPath).reply(HTTP_STATUS_OK, mockProjects);
-
- createComponent();
});
afterEach(() => {
mock.restore();
});
- const findCollapsedEl = () => wrapper.findByTestId('move-collapsed');
- const findFooter = () => wrapper.findByTestId('footer');
- const findHeader = () => wrapper.findByTestId('header');
- const findFailedLoadResults = () => wrapper.findByTestId('failed-load-results');
- const findDropdownContent = () => wrapper.findByTestId('content');
- const findSearchBox = () => wrapper.findComponent(GlSearchBoxByType);
- const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
- const findDropdownEl = () => wrapper.findComponent(GlDropdown);
- const findAllDropdownItems = () => wrapper.findAllComponents(GlDropdownItem);
-
- describe('watch', () => {
- describe('searchKey', () => {
- it('calls `fetchProjects` with value of the prop', async () => {
- jest.spyOn(axios, 'get');
- findSearchBox().vm.$emit('input', 'foo');
-
- await waitForPromises();
-
- expect(axios.get).toHaveBeenCalledWith('/-/autocomplete/projects?project_id=1', {
- params: { search: 'foo' },
- });
- });
- });
- });
-
- describe('methods', () => {
- describe('fetchProjects', () => {
- it('sets projectsListLoading to true and projectsListLoadFailed to false', async () => {
- findDropdownEl().vm.$emit('shown');
- await nextTick();
-
- expect(findLoadingIcon().exists()).toBe(true);
- expect(findFailedLoadResults().exists()).toBe(false);
- });
-
- it('calls `axios.get` with `projectsFetchPath` and query param `search`', async () => {
- jest.spyOn(axios, 'get');
-
- findSearchBox().vm.$emit('input', 'foo');
- await waitForPromises();
-
- expect(axios.get).toHaveBeenCalledWith(
- mockProps.projectsFetchPath,
- expect.objectContaining({
- params: {
- search: 'foo',
- },
- }),
- );
- });
-
- it('sets response to `projects` and focuses on searchInput when request is successful', async () => {
- jest.spyOn(axios, 'get');
-
- findSearchBox().vm.$emit('input', 'foo');
- await waitForPromises();
+ const findDropdownButton = () => wrapper.findByTestId('dropdown-button');
+ const findDropdown = () => wrapper.findComponent(GlCollapsibleListbox);
+ const findDropdownMoveButton = () => wrapper.findByTestId('dropdown-move-button');
+ const findDropdownItemsText = () =>
+ wrapper.findAllComponents(GlListboxItem).wrappers.map((item) => item.text());
- expect(findAllDropdownItems()).toHaveLength(mockProjects.length);
- expect(focusInputMock).toHaveBeenCalled();
- });
-
- it('sets projectsListLoadFailed to true when request fails', async () => {
- jest.spyOn(axios, 'get').mockRejectedValue({});
-
- findSearchBox().vm.$emit('input', 'foo');
- await waitForPromises();
-
- expect(findFailedLoadResults().exists()).toBe(true);
- });
-
- it('sets projectsListLoading to false when request completes', async () => {
- jest.spyOn(axios, 'get');
-
- findDropdownEl().vm.$emit('shown');
- await waitForPromises();
-
- expect(findLoadingIcon().exists()).toBe(false);
- });
- });
-
- describe('isSelectedProject', () => {
- it.each`
- projectIndex | selectedProjectIndex | title | returnValue
- ${0} | ${0} | ${'are same projects'} | ${true}
- ${0} | ${1} | ${'are different projects'} | ${false}
- `(
- 'returns $returnValue when selectedProject and provided project param $title',
- async ({ projectIndex, selectedProjectIndex, returnValue }) => {
- findDropdownEl().vm.$emit('shown');
- await waitForPromises();
-
- findAllDropdownItems().at(selectedProjectIndex).vm.$emit('click', mockEvent);
-
- await nextTick();
-
- expect(findAllDropdownItems().at(projectIndex).props('isChecked')).toBe(returnValue);
- },
- );
-
- it('returns false when selectedProject is null', async () => {
- findDropdownEl().vm.$emit('shown');
- await waitForPromises();
+ it('renders a dropdown button with provided title and header', () => {
+ createComponent();
- expect(findAllDropdownItems().at(0).props('isChecked')).toBe(false);
- });
- });
+ expect(findDropdownButton().text()).toBe(mockProps.dropdownButtonTitle);
+ expect(findDropdown().props('headerText')).toBe(mockProps.dropdownHeaderTitle);
});
- describe('template', () => {
- it('renders collapsed state element with icon', () => {
- const collapsedEl = findCollapsedEl();
-
- expect(collapsedEl.exists()).toBe(true);
- expect(collapsedEl.attributes('title')).toBe(mockProps.dropdownButtonTitle);
- expect(collapsedEl.findComponent(GlIcon).exists()).toBe(true);
- expect(collapsedEl.findComponent(GlIcon).props('name')).toBe('arrow-right');
- });
-
- describe('gl-dropdown component', () => {
- it('renders component container element', () => {
- expect(findDropdownEl().exists()).toBe(true);
- expect(findDropdownEl().props('block')).toBe(true);
- });
+ it('renders the dropdown button as disabled when disabled prop is true', () => {
+ createComponent({ disabled: true });
- it('renders gl-dropdown-form component', () => {
- expect(findDropdownEl().findComponent(GlDropdownForm).exists()).toBe(true);
- });
-
- it('renders disabled dropdown when `disabled` is true', () => {
- createComponent({ ...mockProps, disabled: true });
- expect(findDropdownEl().props('disabled')).toBe(true);
- });
-
- it('renders header element', () => {
- const headerEl = findHeader();
-
- expect(headerEl.exists()).toBe(true);
- expect(headerEl.find('span').text()).toBe(mockProps.dropdownHeaderTitle);
- expect(headerEl.findComponent(GlButton).props('icon')).toBe('close');
- });
-
- it('renders gl-search-box-by-type component', () => {
- const searchEl = findDropdownEl().findComponent(GlSearchBoxByType);
-
- expect(searchEl.exists()).toBe(true);
- expect(searchEl.attributes()).toMatchObject({
- placeholder: 'Search project',
- debounce: '300',
- });
- });
-
- it('renders gl-loading-icon component when projectsListLoading prop is true', async () => {
- findDropdownEl().vm.$emit('shown');
- await nextTick();
-
- expect(findLoadingIcon().exists()).toBe(true);
- });
-
- it('renders gl-dropdown-item components for available projects', async () => {
- findDropdownEl().vm.$emit('shown');
- await waitForPromises();
-
- findAllDropdownItems().at(0).vm.$emit('click', mockEvent);
- await nextTick();
-
- expect(findAllDropdownItems()).toHaveLength(mockProjects.length);
- expect(findAllDropdownItems().at(0).props()).toMatchObject({
- isCheckItem: true,
- isChecked: true,
- });
- expect(findAllDropdownItems().at(0).text()).toBe(mockProjects[0].name_with_namespace);
- });
-
- it('renders string "No matching results" when search does not yield any matches', async () => {
- mock.onGet(mockProps.projectsFetchPath).reply(HTTP_STATUS_OK, []);
+ expect(findDropdownButton().props('disabled')).toBe(true);
+ });
- findSearchBox().vm.$emit('input', 'foo');
- await waitForPromises();
+ it('triggers a project search when dropdown button is clicked', async () => {
+ createComponent();
- expect(findDropdownContent().text()).toContain('No matching results');
- });
+ await findDropdownButton().trigger('click');
+ await waitForPromises();
- it('renders string "Failed to load projects" when loading projects list fails', async () => {
- mock.onGet(mockProps.projectsFetchPath).reply(HTTP_STATUS_OK, []);
- jest.spyOn(axios, 'get').mockRejectedValue({});
+ expect(mock.history.get).toHaveLength(1);
- findDropdownEl().vm.$emit('shown');
- await waitForPromises();
+ expect(findDropdownItemsText()).toEqual([
+ 'Gitlab Org / Gitlab Shell',
+ 'Gnuwget / Wget2',
+ 'Commit451 / Lab Coat',
+ ]);
+ });
- expect(findDropdownContent().text()).toContain('Failed to load projects');
- });
+ it('shows "No matching results" when no projects are found', async () => {
+ createComponent();
- it('renders gl-button within footer', async () => {
- const moveButtonEl = findFooter().findComponent(GlButton);
+ mock.onGet(mockProps.projectsFetchPath).reply(HTTP_STATUS_OK, []);
- expect(moveButtonEl.text()).toBe('Move');
- expect(moveButtonEl.attributes('disabled')).toBeDefined();
+ await findDropdown().vm.$emit('search', 'foobar');
+ await waitForPromises();
- findDropdownEl().vm.$emit('shown');
- await waitForPromises();
+ expect(findDropdown().text()).toContain('No matching results');
+ expect(findDropdownItemsText()).toEqual([]);
+ });
- findAllDropdownItems().at(0).vm.$emit('click', mockEvent);
- await nextTick();
+ it('shows "Failed to load projects" when request fails', async () => {
+ createComponent();
- expect(findFooter().findComponent(GlButton).attributes('disabled')).not.toBeDefined();
- });
- });
+ mock.onGet(mockProps.projectsFetchPath).networkError();
- describe('events', () => {
- it('collapsed state element emits `toggle-collapse` event on component when clicked', () => {
- findCollapsedEl().trigger('click');
+ await findDropdown().vm.$emit('search', 'foobar');
+ await waitForPromises();
- expect(wrapper.emitted('toggle-collapse')).toHaveLength(1);
- });
+ expect(findDropdown().text()).toContain('Failed to load projects');
+ expect(findDropdownItemsText()).toEqual([]);
+ });
- it('gl-dropdown component calls `fetchProjects` on `shown` event', () => {
- jest.spyOn(axios, 'get');
+ it('disables the Move issuable button if no project is selected', async () => {
+ createComponent();
- findDropdownEl().vm.$emit('shown');
+ await findDropdownButton().trigger('click');
+ await waitForPromises();
- expect(axios.get).toHaveBeenCalled();
- });
+ expect(findDropdownMoveButton().props('disabled')).toBe(true);
+ });
- it('gl-dropdown component prevents dropdown body from closing on `hide` event when `projectItemClick` prop is true', async () => {
- findDropdownEl().vm.$emit('shown');
- await waitForPromises();
+ it('shows search results when search is successful', async () => {
+ createComponent();
- findAllDropdownItems().at(0).vm.$emit('click', mockEvent);
- await nextTick();
+ mock.onGet(mockProps.projectsFetchPath).reply(HTTP_STATUS_OK, [
+ {
+ id: 2,
+ name_with_namespace: 'Gitlab Org / Gitlab Shell',
+ full_path: 'gitlab-org/gitlab-shell',
+ },
+ ]);
- findDropdownEl().vm.$emit('hide', mockEvent);
+ await findDropdown().vm.$emit('search', 'shell');
+ await waitForPromises();
- expect(mockEvent.preventDefault).toHaveBeenCalled();
- });
+ expect(findDropdownItemsText()).toEqual(['Gitlab Org / Gitlab Shell']);
+ });
- it('gl-dropdown component emits `dropdown-close` event on component from `hide` event', () => {
- findDropdownEl().vm.$emit('hide');
+ it('emits "move-issuable" event when Move issuable button is clicked', async () => {
+ createComponent();
- expect(wrapper.emitted('dropdown-close')).toHaveLength(1);
- });
+ await findDropdownButton().trigger('click');
+ await waitForPromises();
- it('close icon in dropdown header closes the dropdown when clicked', async () => {
- findHeader().findComponent(GlButton).vm.$emit('click', mockEvent);
+ await wrapper.findAllComponents(GlListboxItem).wrappers[0].trigger('click');
+ await findDropdownMoveButton().trigger('click');
- await nextTick();
- expect(hideMock).toHaveBeenCalled();
- });
+ expect(wrapper.emitted('move-issuable')).toEqual([[mockProjects[0]]]);
+ });
- it('sets project for clicked gl-dropdown-item to selectedProject', async () => {
- findDropdownEl().vm.$emit('shown');
- await waitForPromises();
+ it('disables the Move issuable button when moveInProgress prop is true', async () => {
+ createComponent({ moveInProgress: true });
- findAllDropdownItems().at(0).vm.$emit('click', mockEvent);
- await nextTick();
+ await findDropdownButton().trigger('click');
+ await waitForPromises();
- expect(findAllDropdownItems().at(0).props('isChecked')).toBe(true);
- });
+ expect(findDropdownMoveButton().props('disabled')).toBe(true);
+ });
- it('hides dropdown and emits `move-issuable` event when move button is clicked', async () => {
- findDropdownEl().vm.$emit('shown');
- await waitForPromises();
+ it('emits "dropdown-close" event when dropdown is hidden', async () => {
+ createComponent();
- findAllDropdownItems().at(0).vm.$emit('click', mockEvent);
- await nextTick();
+ await findDropdownButton().trigger('click');
+ await waitForPromises();
- findFooter().findComponent(GlButton).vm.$emit('click');
+ await findDropdown().vm.$emit('hidden');
- expect(hideMock).toHaveBeenCalled();
- expect(wrapper.emitted('move-issuable')).toHaveLength(1);
- expect(wrapper.emitted('move-issuable')[0]).toEqual([mockProjects[0]]);
- });
- });
+ expect(wrapper.emitted('dropdown-close')).toHaveLength(1);
});
});
diff --git a/spec/frontend/sidebar/components/sidebar_dropdown_widget_spec.js b/spec/frontend/sidebar/components/sidebar_dropdown_widget_spec.js
index 27ab347775a..c1c3c1fea91 100644
--- a/spec/frontend/sidebar/components/sidebar_dropdown_widget_spec.js
+++ b/spec/frontend/sidebar/components/sidebar_dropdown_widget_spec.js
@@ -1,8 +1,8 @@
import { GlLink, GlLoadingIcon, GlSearchBoxByType } from '@gitlab/ui';
-import * as Sentry from '@sentry/browser';
import { shallowMount, mount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import createMockApollo from 'helpers/mock_apollo_helper';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
diff --git a/spec/frontend/silent_mode_settings/components/app_spec.js b/spec/frontend/silent_mode_settings/components/app_spec.js
index 5997bfd1b5f..dfa2b1bfcbb 100644
--- a/spec/frontend/silent_mode_settings/components/app_spec.js
+++ b/spec/frontend/silent_mode_settings/components/app_spec.js
@@ -1,4 +1,4 @@
-import { GlToggle, GlBadge } from '@gitlab/ui';
+import { GlToggle } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import waitForPromises from 'helpers/wait_for_promises';
import { createAlert } from '~/alert';
@@ -29,19 +29,8 @@ describe('SilentModeSettingsApp', () => {
};
const findGlToggle = () => wrapper.findComponent(GlToggle);
- const findGlBadge = () => wrapper.findComponent(GlBadge);
describe('template', () => {
- describe('experiment badge', () => {
- beforeEach(() => {
- createComponent();
- });
-
- it('renders properly', () => {
- expect(findGlBadge().exists()).toBe(true);
- });
- });
-
describe('when silent mode is already enabled', () => {
beforeEach(() => {
createComponent({ isSilentModeEnabled: true });
diff --git a/spec/frontend/snippets/components/snippet_header_spec.js b/spec/frontend/snippets/components/snippet_header_spec.js
index 3932675aa52..1eb5de70e4b 100644
--- a/spec/frontend/snippets/components/snippet_header_spec.js
+++ b/spec/frontend/snippets/components/snippet_header_spec.js
@@ -80,6 +80,7 @@ describe('Snippet header component', () => {
const findAuthorEmoji = () => wrapper.findComponent(GlEmoji);
const findAuthoredMessage = () => wrapper.find('[data-testid="authored-message"]').text();
+ const findAuthorUsername = () => wrapper.find('[data-testid="authored-username"]');
const findButtons = () => wrapper.findAllComponents(GlButton);
const findButtonsAsModel = () =>
findButtons().wrappers.map((x) => ({
@@ -116,6 +117,7 @@ describe('Snippet header component', () => {
project: null,
author: {
name: 'Thor Odinson',
+ username: null,
status: null,
},
blobs: [Blob],
@@ -135,12 +137,24 @@ describe('Snippet header component', () => {
expect(wrapper.find('.detail-page-header').exists()).toBe(true);
});
- it('renders a message showing snippet creation date and author', () => {
+ it('renders a message showing snippet creation date and author full name, without username when not available', () => {
createComponent();
const text = findAuthoredMessage();
expect(text).toContain('Authored 1 month ago by');
expect(text).toContain('Thor Odinson');
+ expect(findAuthorUsername().exists()).toBe(false);
+ });
+
+ it('renders a message showing snippet creation date, author full name and username', () => {
+ snippet.author.username = 'todinson';
+ createComponent();
+
+ const text = findAuthoredMessage();
+ expect(text).toContain('Authored 1 month ago by');
+ expect(text).toContain('Thor Odinson');
+ expect(text).toContain('@todinson');
+ expect(findAuthorUsername().exists()).toBe(true);
});
describe('author status', () => {
diff --git a/spec/frontend/super_sidebar/components/global_search/components/global_search_default_places_spec.js b/spec/frontend/super_sidebar/components/global_search/components/global_search_default_places_spec.js
index f91c8034fe9..d1bec8f8662 100644
--- a/spec/frontend/super_sidebar/components/global_search/components/global_search_default_places_spec.js
+++ b/spec/frontend/super_sidebar/components/global_search/components/global_search_default_places_spec.js
@@ -88,6 +88,19 @@ describe('GlobalSearchDefaultPlaces', () => {
'data-qa-places-item': 'Admin area',
},
},
+ {
+ text: 'Leave admin mode',
+ href: '/admin/session/destroy',
+ extraAttrs: {
+ 'data-track-action': 'click_command_palette_item',
+ 'data-track-extra': '{"title":"Leave admin mode"}',
+ 'data-track-label': 'item_without_id',
+ 'data-track-property': 'nav_panel_unknown',
+ 'data-testid': 'places-item-link',
+ 'data-qa-places-item': 'Leave admin mode',
+ 'data-method': 'post',
+ },
+ },
]);
});
});
diff --git a/spec/frontend/super_sidebar/components/global_search/components/global_search_spec.js b/spec/frontend/super_sidebar/components/global_search/components/global_search_spec.js
index 038c7a96adc..c1258294110 100644
--- a/spec/frontend/super_sidebar/components/global_search/components/global_search_spec.js
+++ b/spec/frontend/super_sidebar/components/global_search/components/global_search_spec.js
@@ -25,7 +25,14 @@ import {
} from '~/super_sidebar/components/global_search/constants';
import { truncate } from '~/lib/utils/text_utility';
import { visitUrl } from '~/lib/utils/url_utility';
-import { ENTER_KEY } from '~/lib/utils/keys';
+import {
+ ENTER_KEY,
+ ARROW_DOWN_KEY,
+ ARROW_UP_KEY,
+ END_KEY,
+ HOME_KEY,
+ NUMPAD_ENTER_KEY,
+} from '~/lib/utils/keys';
import {
MOCK_SEARCH,
MOCK_SEARCH_QUERY,
@@ -415,7 +422,7 @@ describe('GlobalSearchModal', () => {
class="gl-new-dropdown-item"
tabindex="0"
:data-testid="'test-result-' + n"
- >Result {{ n }}</li>
+ ><a href="#">Result {{ n }}</a></li>
</ul>`,
},
},
@@ -429,26 +436,26 @@ describe('GlobalSearchModal', () => {
});
it('Home key keeps focus in input', () => {
- const event = triggerKeydownEvent(findSearchInput().element, 'Home');
+ const event = triggerKeydownEvent(findSearchInput().element, HOME_KEY);
expect(document.activeElement).toBe(findSearchInput().element);
expect(event.defaultPrevented).toBe(false);
});
it('End key keeps focus on input', () => {
- const event = triggerKeydownEvent(findSearchInput().element, 'End');
- findSearchInput().trigger('keydown', { code: 'End' });
+ const event = triggerKeydownEvent(findSearchInput().element, END_KEY);
+ findSearchInput().trigger('keydown', { code: END_KEY });
expect(document.activeElement).toBe(findSearchInput().element);
expect(event.defaultPrevented).toBe(false);
});
it('ArrowUp keeps focus on input', () => {
- const event = triggerKeydownEvent(findSearchInput().element, 'ArrowUp');
+ const event = triggerKeydownEvent(findSearchInput().element, ARROW_UP_KEY);
expect(document.activeElement).toBe(findSearchInput().element);
expect(event.defaultPrevented).toBe(false);
});
it('ArrowDown focuses the first item', () => {
- const event = triggerKeydownEvent(findSearchInput().element, 'ArrowDown');
+ const event = triggerKeydownEvent(findSearchInput().element, ARROW_DOWN_KEY);
expect(document.activeElement).toBe(wrapper.findByTestId('test-result-1').element);
expect(event.defaultPrevented).toBe(true);
});
@@ -460,32 +467,44 @@ describe('GlobalSearchModal', () => {
});
it('Home key focuses first item', () => {
- const event = triggerKeydownEvent(document.activeElement, 'Home');
+ const event = triggerKeydownEvent(document.activeElement, HOME_KEY);
expect(document.activeElement).toBe(wrapper.findByTestId('test-result-1').element);
expect(event.defaultPrevented).toBe(true);
});
it('End key focuses last item', () => {
- const event = triggerKeydownEvent(document.activeElement, 'End');
+ const event = triggerKeydownEvent(document.activeElement, END_KEY);
expect(document.activeElement).toBe(wrapper.findByTestId('test-result-5').element);
expect(event.defaultPrevented).toBe(true);
});
it('ArrowUp focuses previous item if any, else input', () => {
- let event = triggerKeydownEvent(document.activeElement, 'ArrowUp');
+ let event = triggerKeydownEvent(document.activeElement, ARROW_UP_KEY);
expect(document.activeElement).toBe(wrapper.findByTestId('test-result-1').element);
expect(event.defaultPrevented).toBe(true);
- event = triggerKeydownEvent(document.activeElement, 'ArrowUp');
+ event = triggerKeydownEvent(document.activeElement, ARROW_UP_KEY);
expect(document.activeElement).toBe(findSearchInput().element);
expect(event.defaultPrevented).toBe(true);
});
it('ArrowDown focuses next item', () => {
- const event = triggerKeydownEvent(document.activeElement, 'ArrowDown');
+ const event = triggerKeydownEvent(document.activeElement, ARROW_DOWN_KEY);
expect(document.activeElement).toBe(wrapper.findByTestId('test-result-3').element);
expect(event.defaultPrevented).toBe(true);
});
+
+ it('NumpadEnter clicks the current item child', () => {
+ const focusedElement = document.activeElement;
+ const focusedElementChild = focusedElement.firstChild;
+
+ const clickMock = jest.fn();
+ focusedElementChild.click = clickMock;
+
+ const event = triggerKeydownEvent(focusedElement, NUMPAD_ENTER_KEY);
+ expect(clickMock).toHaveBeenCalled();
+ expect(event.defaultPrevented).toBe(true);
+ });
});
});
});
diff --git a/spec/frontend/super_sidebar/components/nav_item_spec.js b/spec/frontend/super_sidebar/components/nav_item_spec.js
index e6de9b1de22..94eb47887c3 100644
--- a/spec/frontend/super_sidebar/components/nav_item_spec.js
+++ b/spec/frontend/super_sidebar/components/nav_item_spec.js
@@ -90,6 +90,19 @@ describe('NavItem component', () => {
expect(findPill().text()).toBe(initialPillValue);
});
});
+
+ describe('async updating pill prop', () => {
+ it('re-renders item with when prop pill_count changes', async () => {
+ createWrapper({ item: { title: 'Foo', pill_count: 0 } });
+
+ expect(findPill().text()).toBe('0');
+
+ // https://gitlab.com/gitlab-org/gitlab/-/issues/428246
+ // This is testing specific async behaviour that was before missed
+ await wrapper.setProps({ item: { title: 'Foo', pill_count: 10 } });
+ expect(findPill().text()).toBe('10');
+ });
+ });
});
describe('destroyed', () => {
diff --git a/spec/frontend/super_sidebar/components/user_bar_spec.js b/spec/frontend/super_sidebar/components/user_bar_spec.js
index b58b65f09f5..27d65f27007 100644
--- a/spec/frontend/super_sidebar/components/user_bar_spec.js
+++ b/spec/frontend/super_sidebar/components/user_bar_spec.js
@@ -49,7 +49,6 @@ describe('UserBar component', () => {
sidebarData,
},
provide: {
- toggleNewNavEndpoint: '/-/profile/preferences',
isImpersonating: false,
...provideOverrides,
},
diff --git a/spec/frontend/super_sidebar/components/user_menu_spec.js b/spec/frontend/super_sidebar/components/user_menu_spec.js
index 79a31492f3f..45a60fce00a 100644
--- a/spec/frontend/super_sidebar/components/user_menu_spec.js
+++ b/spec/frontend/super_sidebar/components/user_menu_spec.js
@@ -3,8 +3,6 @@ import { mountExtended } from 'helpers/vue_test_utils_helper';
import { stubComponent } from 'helpers/stub_component';
import UserMenu from '~/super_sidebar/components/user_menu.vue';
import UserMenuProfileItem from '~/super_sidebar/components/user_menu_profile_item.vue';
-import NewNavToggle from '~/nav/components/new_nav_toggle.vue';
-import invalidUrl from '~/lib/utils/invalid_url';
import { mockTracking } from 'helpers/tracking_helper';
import PersistentUserCallout from '~/persistent_user_callout';
import { userMenuMockData, userMenuMockStatus, userMenuMockPipelineMinutes } from '../mock_data';
@@ -14,7 +12,6 @@ describe('UserMenu component', () => {
let trackingSpy;
const GlEmoji = { template: '<img/>' };
- const toggleNewNavEndpoint = invalidUrl;
const findDropdown = () => wrapper.findComponent(GlDisclosureDropdown);
const showDropdown = () => findDropdown().vm.$emit('shown');
@@ -34,7 +31,6 @@ describe('UserMenu component', () => {
...stubs,
},
provide: {
- toggleNewNavEndpoint,
isImpersonating: false,
...provide,
},
@@ -459,15 +455,6 @@ describe('UserMenu component', () => {
});
});
- describe('New navigation toggle item', () => {
- it('should render menu item with new navigation toggle', () => {
- createWrapper();
- const toggleItem = wrapper.findComponent(NewNavToggle);
- expect(toggleItem.exists()).toBe(true);
- expect(toggleItem.props('endpoint')).toBe(toggleNewNavEndpoint);
- });
- });
-
describe('Sign out group', () => {
const findSignOutGroup = () => wrapper.findByTestId('sign-out-group');
diff --git a/spec/frontend/super_sidebar/mock_data.js b/spec/frontend/super_sidebar/mock_data.js
index d464ce372ed..d2d2faedbf8 100644
--- a/spec/frontend/super_sidebar/mock_data.js
+++ b/spec/frontend/super_sidebar/mock_data.js
@@ -74,6 +74,7 @@ export const mergeRequestMenuGroup = [
export const contextSwitcherLinks = [
{ title: 'Explore', link: '/explore', icon: 'compass', link_classes: 'persistent-link-class' },
{ title: 'Admin area', link: '/admin', icon: 'admin' },
+ { title: 'Leave admin mode', link: '/admin/session/destroy', data_method: 'post' },
];
export const sidebarData = {
diff --git a/spec/frontend/super_sidebar/utils_spec.js b/spec/frontend/super_sidebar/utils_spec.js
index 85c13a4c892..43eb82f5928 100644
--- a/spec/frontend/super_sidebar/utils_spec.js
+++ b/spec/frontend/super_sidebar/utils_spec.js
@@ -1,5 +1,5 @@
-import * as Sentry from '@sentry/browser';
import MockAdapter from 'axios-mock-adapter';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import {
getTopFrequentItems,
trackContextAccess,
@@ -16,7 +16,7 @@ import waitForPromises from 'helpers/wait_for_promises';
import { unsortedFrequentItems, sortedFrequentItems } from '../frequent_items/mock_data';
import { cachedFrequentProjects } from './mock_data';
-jest.mock('@sentry/browser');
+jest.mock('~/sentry/sentry_browser_wrapper');
useLocalStorageSpy();
diff --git a/spec/frontend/terraform/components/init_command_modal_spec.js b/spec/frontend/terraform/components/init_command_modal_spec.js
index 4015482b81b..cdd25e90318 100644
--- a/spec/frontend/terraform/components/init_command_modal_spec.js
+++ b/spec/frontend/terraform/components/init_command_modal_spec.js
@@ -8,13 +8,13 @@ const terraformApiUrl = 'https://gitlab.com/api/v4/projects/1';
const username = 'username';
const modalId = 'fake-modal-id';
const stateName = 'aws/eu-central-1';
-const stateNamePlaceholder = '<YOUR-STATE-NAME>';
const stateNameEncoded = encodeURIComponent(stateName);
const modalInfoCopyStr = `export GITLAB_ACCESS_TOKEN=<YOUR-ACCESS-TOKEN>
+export TF_STATE_NAME=${stateNameEncoded}
terraform init \\
- -backend-config="address=${terraformApiUrl}/${stateNameEncoded}" \\
- -backend-config="lock_address=${terraformApiUrl}/${stateNameEncoded}/lock" \\
- -backend-config="unlock_address=${terraformApiUrl}/${stateNameEncoded}/lock" \\
+ -backend-config="address=${terraformApiUrl}/$TF_STATE_NAME" \\
+ -backend-config="lock_address=${terraformApiUrl}/$TF_STATE_NAME/lock" \\
+ -backend-config="unlock_address=${terraformApiUrl}/$TF_STATE_NAME/lock" \\
-backend-config="username=${username}" \\
-backend-config="password=$GITLAB_ACCESS_TOKEN" \\
-backend-config="lock_method=POST" \\
@@ -67,7 +67,7 @@ describe('InitCommandModal', () => {
describe('init command', () => {
it('includes correct address', () => {
expect(findInitCommand().text()).toContain(
- `-backend-config="address=${terraformApiUrl}/${stateNameEncoded}"`,
+ `-backend-config="address=${terraformApiUrl}/$TF_STATE_NAME"`,
);
});
it('includes correct username', () => {
@@ -94,7 +94,7 @@ describe('InitCommandModal', () => {
describe('on rendering', () => {
it('includes correct address', () => {
expect(findInitCommand().text()).toContain(
- `-backend-config="address=${terraformApiUrl}/${stateNamePlaceholder}"`,
+ `-backend-config="address=${terraformApiUrl}/$TF_STATE_NAME"`,
);
});
});
diff --git a/spec/frontend/time_tracking/components/timelogs_app_spec.js b/spec/frontend/time_tracking/components/timelogs_app_spec.js
index 13188f3b937..4cc719ee09f 100644
--- a/spec/frontend/time_tracking/components/timelogs_app_spec.js
+++ b/spec/frontend/time_tracking/components/timelogs_app_spec.js
@@ -1,10 +1,10 @@
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
-import * as Sentry from '@sentry/browser';
import { GlDatepicker, GlLoadingIcon, GlKeysetPagination } from '@gitlab/ui';
import getTimelogsEmptyResponse from 'test_fixtures/graphql/get_timelogs_empty_response.json';
import getPaginatedTimelogsResponse from 'test_fixtures/graphql/get_paginated_timelogs_response.json';
import getNonPaginatedTimelogsResponse from 'test_fixtures/graphql/get_non_paginated_timelogs_response.json';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import { createAlert } from '~/alert';
import { mountExtended, extendedWrapper } from 'helpers/vue_test_utils_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
@@ -14,7 +14,7 @@ import TimelogsApp from '~/time_tracking/components/timelogs_app.vue';
import TimelogsTable from '~/time_tracking/components/timelogs_table.vue';
jest.mock('~/alert');
-jest.mock('@sentry/browser');
+jest.mock('~/sentry/sentry_browser_wrapper');
describe('Timelogs app', () => {
Vue.use(VueApollo);
diff --git a/spec/frontend/token_access/token_projects_table_spec.js b/spec/frontend/token_access/token_projects_table_spec.js
index 7654aa09b0a..7a78befa0d7 100644
--- a/spec/frontend/token_access/token_projects_table_spec.js
+++ b/spec/frontend/token_access/token_projects_table_spec.js
@@ -28,7 +28,6 @@ describe('Token projects table', () => {
const findAllDeleteProjectBtn = () => wrapper.findAllComponents(GlButton);
const findAllTableRows = () => wrapper.findAllByTestId('projects-token-table-row');
const findProjectNameCell = () => wrapper.findByTestId('token-access-project-name');
- const findProjectNamespaceCell = () => wrapper.findByTestId('token-access-project-namespace');
it('displays a table', () => {
createComponent();
@@ -57,25 +56,9 @@ describe('Token projects table', () => {
expect(findAllDeleteProjectBtn()).toHaveLength(1);
});
- it('displays project and namespace cells', () => {
+ it('displays project fullpath', () => {
createComponent();
- expect(findProjectNameCell().text()).toBe('merge-train-stuff');
- expect(findProjectNamespaceCell().text()).toBe('root');
- });
-
- it('displays empty string for namespace when namespace is null', () => {
- const nullNamespace = {
- id: '1',
- name: 'merge-train-stuff',
- namespace: null,
- fullPath: 'root/merge-train-stuff',
- isLocked: false,
- __typename: 'Project',
- };
-
- createComponent({ projects: [nullNamespace] });
-
- expect(findProjectNamespaceCell().text()).toBe('');
+ expect(findProjectNameCell().text()).toBe('root/merge-train-stuff');
});
});
diff --git a/spec/frontend/tracking/dispatch_snowplow_event_spec.js b/spec/frontend/tracking/dispatch_snowplow_event_spec.js
index 5f4d065d504..8297a7088f2 100644
--- a/spec/frontend/tracking/dispatch_snowplow_event_spec.js
+++ b/spec/frontend/tracking/dispatch_snowplow_event_spec.js
@@ -1,10 +1,10 @@
-import * as Sentry from '@sentry/browser';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import { dispatchSnowplowEvent } from '~/tracking/dispatch_snowplow_event';
import getStandardContext from '~/tracking/get_standard_context';
import { extraContext, servicePingContext } from './mock_data';
-jest.mock('@sentry/browser');
+jest.mock('~/sentry/sentry_browser_wrapper');
jest.mock('~/tracking/get_standard_context');
const category = 'Incident Management';
diff --git a/spec/frontend/tracking/tracking_initialization_spec.js b/spec/frontend/tracking/tracking_initialization_spec.js
index 2dc3c6ab41c..adaac7441f0 100644
--- a/spec/frontend/tracking/tracking_initialization_spec.js
+++ b/spec/frontend/tracking/tracking_initialization_spec.js
@@ -1,6 +1,7 @@
import { TRACKING_CONTEXT_SCHEMA } from '~/experimentation/constants';
import { getExperimentData, getAllExperimentContexts } from '~/experimentation/utils';
import Tracking, { initUserTracking, initDefaultTrackers, InternalEvents } from '~/tracking';
+import { MAX_LOCAL_STORAGE_QUEUE_SIZE } from '~/tracking/constants';
import getStandardContext from '~/tracking/get_standard_context';
jest.mock('~/experimentation/utils', () => ({
@@ -65,6 +66,7 @@ describe('Tracking', () => {
fields: { allow: [] },
forms: { allow: [] },
},
+ maxLocalStorageQueueSize: MAX_LOCAL_STORAGE_QUEUE_SIZE,
});
});
});
diff --git a/spec/frontend/users/profile/components/report_abuse_button_spec.js b/spec/frontend/users/profile/components/report_abuse_button_spec.js
deleted file mode 100644
index 1ca944dce12..00000000000
--- a/spec/frontend/users/profile/components/report_abuse_button_spec.js
+++ /dev/null
@@ -1,79 +0,0 @@
-import { GlButton } from '@gitlab/ui';
-import { createWrapper } from '@vue/test-utils';
-import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants';
-import ReportAbuseButton from '~/users/profile/components/report_abuse_button.vue';
-import AbuseCategorySelector from '~/abuse_reports/components/abuse_category_selector.vue';
-
-describe('ReportAbuseButton', () => {
- let wrapper;
-
- const ACTION_PATH = '/abuse_reports/add_category';
- const USER_ID = 1;
- const REPORTED_FROM_URL = 'http://example.com';
-
- const createComponent = (props) => {
- wrapper = shallowMountExtended(ReportAbuseButton, {
- propsData: {
- ...props,
- },
- provide: {
- reportAbusePath: ACTION_PATH,
- reportedUserId: USER_ID,
- reportedFromUrl: REPORTED_FROM_URL,
- },
- });
- };
-
- beforeEach(() => {
- createComponent();
- });
-
- const findReportAbuseButton = () => wrapper.findComponent(GlButton);
- const findAbuseCategorySelector = () => wrapper.findComponent(AbuseCategorySelector);
-
- it('renders report abuse button', () => {
- expect(findReportAbuseButton().exists()).toBe(true);
-
- expect(findReportAbuseButton().props()).toMatchObject({
- category: 'primary',
- icon: 'error',
- });
-
- expect(findReportAbuseButton().attributes('aria-label')).toBe(
- ReportAbuseButton.i18n.reportAbuse,
- );
- });
-
- it('renders abuse category selector with the drawer initially closed', () => {
- expect(findAbuseCategorySelector().exists()).toBe(true);
-
- expect(findAbuseCategorySelector().props('showDrawer')).toBe(false);
- });
-
- describe('when button is clicked', () => {
- beforeEach(async () => {
- await findReportAbuseButton().vm.$emit('click');
- });
-
- it('opens the abuse category selector', () => {
- expect(findAbuseCategorySelector().props('showDrawer')).toBe(true);
- });
-
- it('closes the abuse category selector', async () => {
- await findAbuseCategorySelector().vm.$emit('close-drawer');
-
- expect(findAbuseCategorySelector().props('showDrawer')).toBe(false);
- });
- });
-
- describe('when user hovers out of the button', () => {
- it(`should emit ${BV_HIDE_TOOLTIP} to close the tooltip`, () => {
- const rootWrapper = createWrapper(wrapper.vm.$root);
-
- findReportAbuseButton().vm.$emit('mouseout');
-
- expect(rootWrapper.emitted(BV_HIDE_TOOLTIP)).toHaveLength(1);
- });
- });
-});
diff --git a/spec/frontend/vue_merge_request_widget/components/approvals/approvals_spec.js b/spec/frontend/vue_merge_request_widget/components/approvals/approvals_spec.js
index 2aed037be6f..c81f4328d2a 100644
--- a/spec/frontend/vue_merge_request_widget/components/approvals/approvals_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/approvals/approvals_spec.js
@@ -4,6 +4,7 @@ import { GlButton, GlSprintf } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { createMockSubscription as createMockApolloSubscription } from 'mock-apollo-client';
import approvedByCurrentUser from 'test_fixtures/graphql/merge_requests/approvals/approvals.query.graphql.json';
+import { visitUrl } from '~/lib/utils/url_utility';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
@@ -28,6 +29,10 @@ jest.mock('~/alert', () => ({
dismiss: mockAlertDismiss,
})),
}));
+jest.mock('~/lib/utils/url_utility', () => ({
+ ...jest.requireActual('~/lib/utils/url_utility'),
+ visitUrl: jest.fn(),
+}));
const TEST_HELP_PATH = 'help/path';
const testApprovedBy = () => [1, 7, 10].map((id) => ({ id }));
@@ -113,6 +118,7 @@ describe('MRWidget approvals', () => {
targetProjectFullPath: 'gitlab-org/gitlab',
id: 1,
iid: '1',
+ requireSamlAuthToApprove: false,
};
jest.spyOn(eventHub, '$emit').mockImplementation(() => {});
@@ -172,6 +178,22 @@ describe('MRWidget approvals', () => {
category: 'primary',
});
});
+
+ describe('with SAML auth requried for approval', () => {
+ beforeEach(async () => {
+ const response = createCanApproveResponse();
+ mr.requireSamlAuthToApprove = true;
+ createComponent({}, { query: response });
+ await waitForPromises();
+ });
+ it('approve action is rendered with correct text', () => {
+ expect(findActionData()).toEqual({
+ variant: 'confirm',
+ text: 'Approve with SAML',
+ category: 'primary',
+ });
+ });
+ });
});
describe('and MR is approved', () => {
@@ -194,6 +216,25 @@ describe('MRWidget approvals', () => {
});
});
+ describe('with approvers, with SAML auth requried for approval', () => {
+ beforeEach(async () => {
+ canApproveResponse.data.project.mergeRequest.approvedBy.nodes =
+ approvedByCurrentUser.data.project.mergeRequest.approvedBy.nodes;
+ canApproveResponse.data.project.mergeRequest.approvedBy.nodes[0].id = 69;
+ mr.requireSamlAuthToApprove = true;
+ createComponent({}, { query: canApproveResponse });
+ await waitForPromises();
+ });
+
+ it('approve additionally action is rendered with correct text', () => {
+ expect(findActionData()).toEqual({
+ variant: 'confirm',
+ text: 'Approve additionally with SAML',
+ category: 'secondary',
+ });
+ });
+ });
+
describe('with approvers', () => {
beforeEach(async () => {
canApproveResponse.data.project.mergeRequest.approvedBy.nodes =
@@ -215,6 +256,25 @@ describe('MRWidget approvals', () => {
});
});
+ describe('when SAML auth is required and user clicks Approve with SAML', () => {
+ const fakeGroupSamlPath = '/example_group_saml';
+
+ beforeEach(async () => {
+ mr.requireSamlAuthToApprove = true;
+ mr.samlApprovalPath = fakeGroupSamlPath;
+
+ createComponent({}, { query: createCanApproveResponse() });
+ await waitForPromises();
+ });
+
+ it('redirects the user to the group SAML path', async () => {
+ const action = findAction();
+ action.vm.$emit('click');
+ await nextTick();
+ expect(visitUrl).toHaveBeenCalledWith(fakeGroupSamlPath);
+ });
+ });
+
describe('when approve action is clicked', () => {
beforeEach(async () => {
createComponent({}, { query: canApproveResponse });
diff --git a/spec/frontend/vue_merge_request_widget/components/checks/conflicts_spec.js b/spec/frontend/vue_merge_request_widget/components/checks/conflicts_spec.js
index 57dcd2fd819..ad7c14ddae6 100644
--- a/spec/frontend/vue_merge_request_widget/components/checks/conflicts_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/checks/conflicts_spec.js
@@ -12,7 +12,7 @@ let wrapper;
let apolloProvider;
function factory({
- result = 'passed',
+ status = 'success',
canMerge = true,
pushToSourceBranch = true,
shouldBeRebased = false,
@@ -42,7 +42,7 @@ function factory({
apolloProvider,
propsData: {
mr,
- check: { result, failureReason: 'Conflicts message' },
+ check: { status, identifier: 'CONFLICT' },
},
});
}
@@ -55,7 +55,7 @@ describe('Merge request merge checks conflicts component', () => {
it('renders failure reason text', () => {
factory();
- expect(wrapper.text()).toEqual('Conflicts message');
+ expect(wrapper.text()).toEqual('Merge conflicts must be resolved.');
});
it.each`
@@ -74,7 +74,12 @@ describe('Merge request merge checks conflicts component', () => {
sourceBranchProtected,
rendersConflictButton,
}) => {
- factory({ mr: { conflictResolutionPath }, pushToSourceBranch, sourceBranchProtected });
+ factory({
+ status: 'FAILED',
+ mr: { conflictResolutionPath },
+ pushToSourceBranch,
+ sourceBranchProtected,
+ });
await waitForPromises();
diff --git a/spec/frontend/vue_merge_request_widget/components/checks/message_spec.js b/spec/frontend/vue_merge_request_widget/components/checks/message_spec.js
index 4446eb7324b..aeea34c29ce 100644
--- a/spec/frontend/vue_merge_request_widget/components/checks/message_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/checks/message_spec.js
@@ -12,18 +12,18 @@ function factory(propsData = {}) {
describe('Merge request merge checks message component', () => {
it('renders failure reason text', () => {
- factory({ check: { result: 'passed', failureReason: 'Failed message' } });
+ factory({ check: { status: 'success', identifier: 'discussions_not_resolved' } });
- expect(wrapper.text()).toEqual('Failed message');
+ expect(wrapper.text()).toEqual('Unresolved discussions must be resolved.');
});
it.each`
- result | icon
- ${'passed'} | ${'success'}
- ${'failed'} | ${'failed'}
- ${'allowed_to_fail'} | ${'neutral'}
- `('renders $icon icon for $result result', ({ result, icon }) => {
- factory({ check: { result, failureReason: 'Failed message' } });
+ status | icon
+ ${'success'} | ${'success'}
+ ${'failed'} | ${'failed'}
+ ${'inactive'} | ${'neutral'}
+ `('renders $icon icon for $status result', ({ status, icon }) => {
+ factory({ check: { status, identifier: 'discussions_not_resolved' } });
expect(wrapper.findComponent(StatusIcon).props('iconName')).toBe(icon);
});
diff --git a/spec/frontend/vue_merge_request_widget/components/checks/rebase_spec.js b/spec/frontend/vue_merge_request_widget/components/checks/rebase_spec.js
new file mode 100644
index 00000000000..d6c01aee3b1
--- /dev/null
+++ b/spec/frontend/vue_merge_request_widget/components/checks/rebase_spec.js
@@ -0,0 +1,323 @@
+import Vue, { nextTick } from 'vue';
+import VueApollo from 'vue-apollo';
+import { GlModal } from '@gitlab/ui';
+import MergeChecksRebase from '~/vue_merge_request_widget/components/checks/rebase.vue';
+import rebaseQuery from '~/vue_merge_request_widget/queries/states/rebase.query.graphql';
+import eventHub from '~/vue_merge_request_widget/event_hub';
+import toast from '~/vue_shared/plugins/global_toast';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import { stubComponent } from 'helpers/stub_component';
+
+jest.mock('~/vue_shared/plugins/global_toast');
+
+let wrapper;
+const showMock = jest.fn();
+
+const mockPipelineNodes = [
+ {
+ id: '1',
+ project: {
+ id: '2',
+ fullPath: 'user/forked',
+ },
+ },
+];
+
+const mockQueryHandler = ({
+ rebaseInProgress = false,
+ targetBranch = '',
+ pushToSourceBranch = false,
+ nodes = mockPipelineNodes,
+} = {}) =>
+ jest.fn().mockResolvedValue({
+ data: {
+ project: {
+ id: '1',
+ mergeRequest: {
+ id: '2',
+ rebaseInProgress,
+ targetBranch,
+ userPermissions: {
+ pushToSourceBranch,
+ },
+ pipelines: {
+ nodes,
+ },
+ },
+ },
+ },
+ });
+
+const createMockApolloProvider = (handler) => {
+ Vue.use(VueApollo);
+
+ return createMockApollo([[rebaseQuery, handler]]);
+};
+
+function createWrapper({ propsData = {}, provideData = {}, handler = mockQueryHandler() } = {}) {
+ wrapper = mountExtended(MergeChecksRebase, {
+ apolloProvider: createMockApolloProvider(handler),
+ provide: {
+ ...provideData,
+ },
+ propsData: {
+ mr: {},
+ service: {},
+ check: {
+ identifier: 'need_rebase',
+ status: 'FAILED',
+ },
+ ...propsData,
+ },
+ stubs: {
+ GlModal: stubComponent(GlModal, {
+ methods: {
+ show: showMock,
+ },
+ }),
+ },
+ });
+}
+
+describe('Merge request merge checks rebase component', () => {
+ const findStandardRebaseButton = () => wrapper.findByTestId('standard-rebase-button');
+ const findRebaseWithoutCiButton = () => wrapper.findByTestId('rebase-without-ci-button');
+ const findModal = () => wrapper.findComponent(GlModal);
+
+ describe('with permissions', () => {
+ const rebaseMock = jest.fn().mockResolvedValue();
+ const pollMock = jest.fn().mockResolvedValue({});
+
+ describe('Rebase buttons', () => {
+ it('renders both buttons', async () => {
+ createWrapper({
+ handler: mockQueryHandler({ pushToSourceBranch: true }),
+ });
+
+ await waitForPromises();
+
+ expect(findRebaseWithoutCiButton().exists()).toBe(true);
+ expect(findStandardRebaseButton().exists()).toBe(true);
+ });
+
+ it('starts the rebase when clicking', async () => {
+ createWrapper({
+ propsData: {
+ service: {
+ rebase: rebaseMock,
+ poll: pollMock,
+ },
+ },
+ handler: mockQueryHandler({ pushToSourceBranch: true }),
+ });
+
+ await waitForPromises();
+
+ findStandardRebaseButton().vm.$emit('click');
+
+ expect(rebaseMock).toHaveBeenCalledWith({ skipCi: false });
+ });
+
+ it('starts the CI-skipping rebase when clicking on "Rebase without CI"', async () => {
+ createWrapper({
+ propsData: {
+ service: {
+ rebase: rebaseMock,
+ poll: pollMock,
+ },
+ },
+ handler: mockQueryHandler({ pushToSourceBranch: true }),
+ });
+
+ await waitForPromises();
+
+ findRebaseWithoutCiButton().vm.$emit('click');
+
+ expect(rebaseMock).toHaveBeenCalledWith({ skipCi: true });
+ });
+ });
+
+ describe('Rebase when pipelines must succeed is enabled', () => {
+ beforeEach(async () => {
+ createWrapper({
+ propsData: {
+ mr: {
+ onlyAllowMergeIfPipelineSucceeds: true,
+ },
+ service: {
+ rebase: rebaseMock,
+ poll: pollMock,
+ },
+ },
+ handler: mockQueryHandler({ pushToSourceBranch: true }),
+ });
+
+ await waitForPromises();
+ });
+
+ it('renders only the rebase button', () => {
+ expect(findRebaseWithoutCiButton().exists()).toBe(false);
+ expect(findStandardRebaseButton().exists()).toBe(true);
+ });
+
+ it('starts the rebase when clicking', async () => {
+ findStandardRebaseButton().vm.$emit('click');
+
+ await nextTick();
+
+ expect(rebaseMock).toHaveBeenCalledWith({ skipCi: false });
+ });
+ });
+
+ describe('Rebase when pipelines must succeed and skipped pipelines are considered successful are enabled', () => {
+ beforeEach(async () => {
+ createWrapper({
+ propsData: {
+ mr: {
+ onlyAllowMergeIfPipelineSucceeds: true,
+ allowMergeOnSkippedPipeline: true,
+ },
+ service: {
+ rebase: rebaseMock,
+ poll: pollMock,
+ },
+ },
+ handler: mockQueryHandler({ pushToSourceBranch: true }),
+ });
+
+ await waitForPromises();
+ });
+
+ it('renders both rebase buttons', () => {
+ expect(findRebaseWithoutCiButton().exists()).toBe(true);
+ expect(findStandardRebaseButton().exists()).toBe(true);
+ });
+
+ it('starts the rebase when clicking', async () => {
+ findStandardRebaseButton().vm.$emit('click');
+
+ await nextTick();
+
+ expect(rebaseMock).toHaveBeenCalledWith({ skipCi: false });
+ });
+
+ it('starts the CI-skipping rebase when clicking on "Rebase without CI"', async () => {
+ findRebaseWithoutCiButton().vm.$emit('click');
+
+ await nextTick();
+
+ expect(rebaseMock).toHaveBeenCalledWith({ skipCi: true });
+ });
+ });
+
+ describe('security modal', () => {
+ it('displays modal and rebases after confirming', async () => {
+ createWrapper({
+ propsData: {
+ mr: {
+ sourceProjectFullPath: 'user/forked',
+ targetProjectFullPath: 'root/original',
+ },
+ service: {
+ rebase: rebaseMock,
+ poll: pollMock,
+ },
+ },
+ provideData: { canCreatePipelineInTargetProject: true },
+ handler: mockQueryHandler({ pushToSourceBranch: true }),
+ });
+
+ await waitForPromises();
+
+ findStandardRebaseButton().vm.$emit('click');
+ expect(showMock).toHaveBeenCalled();
+
+ findModal().vm.$emit('primary');
+
+ expect(rebaseMock).toHaveBeenCalled();
+ });
+
+ it('does not display modal', async () => {
+ createWrapper({
+ propsData: {
+ mr: {
+ sourceProjectFullPath: 'user/forked',
+ targetProjectFullPath: 'root/original',
+ },
+ service: {
+ rebase: rebaseMock,
+ poll: pollMock,
+ },
+ },
+ provideData: { canCreatePipelineInTargetProject: false },
+ handler: mockQueryHandler({ pushToSourceBranch: true }),
+ });
+
+ await waitForPromises();
+
+ findStandardRebaseButton().vm.$emit('click');
+
+ expect(showMock).not.toHaveBeenCalled();
+ expect(rebaseMock).toHaveBeenCalled();
+ });
+ });
+ });
+
+ describe('without permissions', () => {
+ const exampleTargetBranch = 'fake-branch-to-test-with';
+
+ it('does render the "Rebase without pipeline" button', async () => {
+ createWrapper({
+ handler: mockQueryHandler({
+ rebaseInProgress: false,
+ pushToSourceBranch: false,
+ targetBranch: exampleTargetBranch,
+ }),
+ });
+
+ await waitForPromises();
+
+ expect(findRebaseWithoutCiButton().exists()).toBe(true);
+ });
+ });
+
+ describe('methods', () => {
+ it('checkRebaseStatus', async () => {
+ jest.spyOn(eventHub, '$emit').mockImplementation(() => {});
+ createWrapper({
+ propsData: {
+ service: {
+ rebase() {
+ return Promise.resolve();
+ },
+ poll() {
+ return Promise.resolve({
+ data: {
+ rebase_in_progress: false,
+ should_be_rebased: false,
+ merge_error: null,
+ },
+ });
+ },
+ },
+ },
+ });
+
+ await waitForPromises();
+
+ findRebaseWithoutCiButton().vm.$emit('click');
+
+ // Wait for the rebase request
+ await nextTick();
+ // Wait for the polling request
+ await nextTick();
+ // Wait for the eventHub to be called
+ await nextTick();
+
+ expect(eventHub.$emit).toHaveBeenCalledWith('MRWidgetRebaseSuccess');
+ expect(toast).toHaveBeenCalledWith('Rebase completed');
+ });
+ });
+});
diff --git a/spec/frontend/vue_merge_request_widget/components/checks/unresolved_discussions_spec.js b/spec/frontend/vue_merge_request_widget/components/checks/unresolved_discussions_spec.js
new file mode 100644
index 00000000000..fc83901b318
--- /dev/null
+++ b/spec/frontend/vue_merge_request_widget/components/checks/unresolved_discussions_spec.js
@@ -0,0 +1,49 @@
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import notesEventHub from '~/notes/event_hub';
+import MergeChecksUnresolvedDiscussions from '~/vue_merge_request_widget/components/checks/unresolved_discussions.vue';
+import MergeChecksMessage from '~/vue_merge_request_widget/components/checks/message.vue';
+
+describe('MergeChecksUnresolvedDiscussions component', () => {
+ let wrapper;
+
+ function createComponent(
+ propsData = {
+ check: {
+ status: 'FAILED',
+ failureReason: 'Failed message',
+ identifier: 'discussions_not_resolved',
+ },
+ },
+ ) {
+ wrapper = mountExtended(MergeChecksUnresolvedDiscussions, {
+ propsData,
+ });
+ }
+
+ it('passes check down to the MergeChecksMessage', () => {
+ const check = {
+ status: 'failed',
+ failureReason: 'Unresolved discussions',
+ identifier: 'discussions_not_resolved',
+ };
+ createComponent({ check });
+
+ expect(wrapper.findComponent(MergeChecksMessage).props('check')).toEqual(check);
+ });
+
+ it('does not show go to first unresolved discussion button with passed state', () => {
+ createComponent({ check: { status: 'success', identifier: 'discussions_not_resolved' } });
+ const button = wrapper.findByRole('button', { name: 'Go to first unresolved thread' });
+ expect(button.exists()).toBe(false);
+ });
+
+ it('triggers go to first discussion action', () => {
+ const callback = jest.fn();
+ notesEventHub.$on('jumpToFirstUnresolvedDiscussion', callback);
+ createComponent();
+
+ wrapper.findByRole('button', { name: 'Go to first unresolved thread' }).trigger('click');
+
+ expect(callback).toHaveBeenCalled();
+ });
+});
diff --git a/spec/frontend/vue_merge_request_widget/components/merge_checks_spec.js b/spec/frontend/vue_merge_request_widget/components/merge_checks_spec.js
index c86fe6d0a10..d39098b27c2 100644
--- a/spec/frontend/vue_merge_request_widget/components/merge_checks_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/merge_checks_spec.js
@@ -1,18 +1,22 @@
import VueApollo from 'vue-apollo';
import Vue from 'vue';
-import { mountExtended } from 'helpers/vue_test_utils_helper';
+import { shallowMountExtended, mountExtended } from 'helpers/vue_test_utils_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import MergeChecksComponent from '~/vue_merge_request_widget/components/merge_checks.vue';
import mergeChecksQuery from '~/vue_merge_request_widget/queries/merge_checks.query.graphql';
import StatusIcon from '~/vue_merge_request_widget/components/extensions/status_icon.vue';
+import StateContainer from '~/vue_merge_request_widget/components/state_container.vue';
+import { COMPONENTS } from '~/vue_merge_request_widget/components/checks/constants';
+import conflictsStateQuery from '~/vue_merge_request_widget/queries/states/conflicts.query.graphql';
+import rebaseStateQuery from '~/vue_merge_request_widget/queries/states/rebase.query.graphql';
Vue.use(VueApollo);
let wrapper;
let apolloProvider;
-function factory({ canMerge = true, mergeChecks = [] } = {}) {
+function factory(mountFn, { canMerge = true, mergeabilityChecks = [] } = {}) {
apolloProvider = createMockApollo([
[
mergeChecksQuery,
@@ -20,28 +24,79 @@ function factory({ canMerge = true, mergeChecks = [] } = {}) {
data: {
project: {
id: 1,
- mergeRequest: { id: 1, userPermissions: { canMerge }, mergeChecks },
+ mergeRequest: { id: 1, userPermissions: { canMerge }, mergeabilityChecks },
},
},
}),
],
+ [
+ conflictsStateQuery,
+ () =>
+ Promise.resolve({
+ data: {
+ project: {
+ id: 1,
+ mergeRequest: {
+ id: 1,
+ shouldBeRebased: false,
+ sourceBranchProtected: false,
+ userPermissions: { pushToSourceBranch: true },
+ },
+ },
+ },
+ }),
+ ],
+ [
+ rebaseStateQuery,
+ () =>
+ Promise.resolve({
+ data: {
+ project: {
+ id: '1',
+ mergeRequest: {
+ id: '2',
+ rebaseInProgress: false,
+ targetBranch: 'main',
+ userPermissions: {
+ pushToSourceBranch: true,
+ },
+ pipelines: {
+ nodes: [
+ {
+ id: '1',
+ project: {
+ id: '2',
+ fullPath: 'gitlab/gitlab',
+ },
+ },
+ ],
+ },
+ },
+ },
+ },
+ }),
+ ],
]);
- wrapper = mountExtended(MergeChecksComponent, {
+ wrapper = mountFn(MergeChecksComponent, {
apolloProvider,
propsData: {
mr: {},
+ service: {},
},
});
}
+const mountComponent = factory.bind(null, mountExtended);
+const shallowMountComponent = factory.bind(null, shallowMountExtended);
+
describe('Merge request merge checks component', () => {
afterEach(() => {
apolloProvider = null;
});
it('renders ready to merge text if user can merge', async () => {
- factory({ canMerge: true });
+ mountComponent({ canMerge: true });
await waitForPromises();
@@ -49,7 +104,7 @@ describe('Merge request merge checks component', () => {
});
it('renders ready to merge by members text if user can not merge', async () => {
- factory({ canMerge: false });
+ mountComponent({ canMerge: false });
await waitForPromises();
@@ -57,11 +112,11 @@ describe('Merge request merge checks component', () => {
});
it.each`
- mergeChecks | text
- ${[{ identifier: 'discussions', result: 'failed' }]} | ${'Merge blocked: 1 check failed'}
- ${[{ identifier: 'discussions', result: 'failed' }, { identifier: 'rebase', result: 'failed' }]} | ${'Merge blocked: 2 checks failed'}
- `('renders $text for $mergeChecks', async ({ mergeChecks, text }) => {
- factory({ mergeChecks });
+ mergeabilityChecks | text
+ ${[{ identifier: 'discussions', status: 'failed' }]} | ${'Merge blocked: 1 check failed'}
+ ${[{ identifier: 'discussions', status: 'failed' }, { identifier: 'rebase', status: 'failed' }]} | ${'Merge blocked: 2 checks failed'}
+ `('renders $text for $mergeabilityChecks', async ({ mergeabilityChecks, text }) => {
+ mountComponent({ mergeabilityChecks });
await waitForPromises();
@@ -69,19 +124,37 @@ describe('Merge request merge checks component', () => {
});
it.each`
- result | statusIcon
+ status | statusIcon
${'failed'} | ${'failed'}
${'passed'} | ${'success'}
- `('renders $statusIcon for $result result', async ({ result, statusIcon }) => {
- factory({ mergeChecks: [{ result, identifier: 'discussions' }] });
+ `('renders $statusIcon for $status result', async ({ status, statusIcon }) => {
+ mountComponent({ mergeabilityChecks: [{ status, identifier: 'discussions' }] });
await waitForPromises();
expect(wrapper.findComponent(StatusIcon).props('iconName')).toBe(statusIcon);
});
+ it.each`
+ identifier
+ ${'conflict'}
+ ${'unresolved_discussions'}
+ ${'need_rebase'}
+ ${'default'}
+ `('renders $identifier merge check', async ({ identifier }) => {
+ shallowMountComponent({ mergeabilityChecks: [{ status: 'failed', identifier }] });
+
+ wrapper.findComponent(StateContainer).vm.$emit('toggle');
+
+ await waitForPromises();
+
+ const { default: component } = await COMPONENTS[identifier]();
+
+ expect(wrapper.findComponent(component).exists()).toBe(true);
+ });
+
it('expands collapsed area', async () => {
- factory();
+ mountComponent();
await waitForPromises();
diff --git a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_ready_to_merge_spec.js b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_ready_to_merge_spec.js
index 48b86d879ad..9239807ae71 100644
--- a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_ready_to_merge_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_ready_to_merge_spec.js
@@ -831,4 +831,16 @@ describe('ReadyToMerge', () => {
});
});
});
+
+ describe('merge details', () => {
+ it('shows auto-merge hint when auto merge is set and some checks have failed', () => {
+ createComponent({ mr: { state: 'mergeChecksFailed', autoMergeEnabled: true } });
+ expect(wrapper.text()).toContain('Auto-merge enabled');
+ });
+
+ it("doesn't show auto-merge hint when auto merge is not set", () => {
+ createComponent({ mr: { autoMergeEnabled: false } });
+ expect(wrapper.text()).not.toContain('Auto-merge enabled');
+ });
+ });
});
diff --git a/spec/frontend/vue_merge_request_widget/components/widget/app_spec.js b/spec/frontend/vue_merge_request_widget/components/widget/app_spec.js
index 205824c3edd..1fc3b0c84ee 100644
--- a/spec/frontend/vue_merge_request_widget/components/widget/app_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/widget/app_spec.js
@@ -5,6 +5,7 @@ import MrSecurityWidgetCE from '~/vue_merge_request_widget/extensions/security_r
import MrTestReportWidget from '~/vue_merge_request_widget/extensions/test_report/index.vue';
import MrTerraformWidget from '~/vue_merge_request_widget/extensions/terraform/index.vue';
import MrCodeQualityWidget from '~/vue_merge_request_widget/extensions/code_quality/index.vue';
+import MrAccessibilityWidget from '~/vue_merge_request_widget/extensions/accessibility/index.vue';
describe('MR Widget App', () => {
let wrapper;
@@ -38,10 +39,11 @@ describe('MR Widget App', () => {
});
describe.each`
- widgetName | widget | endpoint
- ${'testReportWidget'} | ${MrTestReportWidget} | ${'testResultsPath'}
- ${'terraformPlansWidget'} | ${MrTerraformWidget} | ${'terraformReportsPath'}
- ${'codeQualityWidget'} | ${MrCodeQualityWidget} | ${'codequalityReportsPath'}
+ widgetName | widget | endpoint
+ ${'testReportWidget'} | ${MrTestReportWidget} | ${'testResultsPath'}
+ ${'terraformPlansWidget'} | ${MrTerraformWidget} | ${'terraformReportsPath'}
+ ${'codeQualityWidget'} | ${MrCodeQualityWidget} | ${'codequalityReportsPath'}
+ ${'accessibilityWidget'} | ${MrAccessibilityWidget} | ${'accessibilityReportPath'}
`('$widgetName', ({ widget, endpoint }) => {
it(`is mounted when ${endpoint} is defined`, async () => {
createComponent({ mr: { [endpoint]: `path/to/${endpoint}` } });
diff --git a/spec/frontend/vue_merge_request_widget/components/widget/dynamic_content_spec.js b/spec/frontend/vue_merge_request_widget/components/widget/dynamic_content_spec.js
index 16751bcc0f0..213959fe4e2 100644
--- a/spec/frontend/vue_merge_request_widget/components/widget/dynamic_content_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/widget/dynamic_content_spec.js
@@ -2,6 +2,7 @@ import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { EXTENSION_ICONS } from '~/vue_merge_request_widget/constants';
import DynamicContent from '~/vue_merge_request_widget/components/widget/dynamic_content.vue';
import ContentRow from '~/vue_merge_request_widget/components/widget/widget_content_row.vue';
+import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
describe('~/vue_merge_request_widget/components/widget/dynamic_content.vue', () => {
let wrapper;
@@ -16,10 +17,13 @@ describe('~/vue_merge_request_widget/components/widget/dynamic_content.vue', ()
DynamicContent,
ContentRow,
},
+ directives: {
+ GlTooltip: createMockDirective('gl-tooltip'),
+ },
});
};
- it('renders given data', () => {
+ beforeEach(() => {
createComponent({
propsData: {
data: {
@@ -49,10 +53,23 @@ describe('~/vue_merge_request_widget/components/widget/dynamic_content.vue', ()
text: 'This is recursive. It will be listed in level 3.',
},
],
+ tooltipText: 'Tooltip text',
},
},
});
+ });
+ it('renders given data', () => {
expect(wrapper.html()).toMatchSnapshot();
});
+
+ it('has a tooltip on the row text', () => {
+ const text = wrapper.findByText('Main text for the row');
+ const tooltip = getBinding(text.element, 'gl-tooltip');
+
+ expect(tooltip.value).toMatchObject({
+ title: 'Tooltip text',
+ boundary: 'viewport',
+ });
+ });
});
diff --git a/spec/frontend/vue_merge_request_widget/components/widget/widget_spec.js b/spec/frontend/vue_merge_request_widget/components/widget/widget_spec.js
index 18fdba32f52..87c1ad7947e 100644
--- a/spec/frontend/vue_merge_request_widget/components/widget/widget_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/widget/widget_spec.js
@@ -1,5 +1,5 @@
import { nextTick } from 'vue';
-import * as Sentry from '@sentry/browser';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import { shallowMountExtended, mountExtended } from 'helpers/vue_test_utils_helper';
import HelpPopover from '~/vue_shared/components/help_popover.vue';
import waitForPromises from 'helpers/wait_for_promises';
diff --git a/spec/frontend/vue_merge_request_widget/extensions/security_reports/mr_widget_security_reports_spec.js b/spec/frontend/vue_merge_request_widget/extensions/security_reports/mr_widget_security_reports_spec.js
index e23cd92f53e..b277a9f6716 100644
--- a/spec/frontend/vue_merge_request_widget/extensions/security_reports/mr_widget_security_reports_spec.js
+++ b/spec/frontend/vue_merge_request_widget/extensions/security_reports/mr_widget_security_reports_spec.js
@@ -1,5 +1,5 @@
import Vue from 'vue';
-import { GlDropdown } from '@gitlab/ui';
+import { GlDisclosureDropdown } from '@gitlab/ui';
import VueApollo from 'vue-apollo';
import MRSecurityWidget from '~/vue_merge_request_widget/extensions/security_reports/mr_widget_security_reports.vue';
import Widget from '~/vue_merge_request_widget/components/widget/widget.vue';
@@ -27,8 +27,7 @@ describe('vue_merge_request_widget/extensions/security_reports/mr_widget_securit
};
const findWidget = () => wrapper.findComponent(Widget);
- const findDropdown = () => wrapper.findComponent(GlDropdown);
- const findDropdownItem = (name) => wrapper.findByTestId(name);
+ const findDropdown = () => wrapper.findComponent(GlDisclosureDropdown);
describe('with data', () => {
beforeEach(async () => {
@@ -55,24 +54,52 @@ describe('vue_merge_request_widget/extensions/security_reports/mr_widget_securit
});
it.each`
- artifactName | exists | downloadPath
- ${'sam_scan'} | ${true} | ${'/root/security-reports/-/jobs/14/artifacts/download?file_type=sast'}
- ${'sast-spotbugs'} | ${true} | ${'/root/security-reports/-/jobs/11/artifacts/download?file_type=sast'}
- ${'sast-sobelow'} | ${false} | ${''}
- ${'sast-pmd-apex'} | ${false} | ${''}
- ${'sast-eslint'} | ${true} | ${'/root/security-reports/-/jobs/8/artifacts/download?file_type=sast'}
- ${'secrets'} | ${true} | ${'/root/security-reports/-/jobs/7/artifacts/download?file_type=secret_detection'}
+ artifactName | downloadPath
+ ${'sam_scan'} | ${'/root/security-reports/-/jobs/14/artifacts/download?file_type=sast'}
+ ${'sast-spotbugs'} | ${'/root/security-reports/-/jobs/11/artifacts/download?file_type=sast'}
+ ${'sast-eslint'} | ${'/root/security-reports/-/jobs/8/artifacts/download?file_type=sast'}
+ ${'secrets'} | ${'/root/security-reports/-/jobs/7/artifacts/download?file_type=secret_detection'}
`(
- 'has a dropdown to download $artifactName artifacts',
- ({ artifactName, exists, downloadPath }) => {
+ 'has a dropdown item to download $artifactName artifacts',
+ ({ artifactName, downloadPath }) => {
expect(findDropdown().exists()).toBe(true);
- expect(wrapper.findByText(`Download ${artifactName}`).exists()).toBe(exists);
- if (exists) {
- const dropdownItem = findDropdownItem(`download-${artifactName}`);
- expect(dropdownItem.attributes('download')).toBe('');
- expect(dropdownItem.attributes('href')).toBe(downloadPath);
- }
+ expect(findDropdown().props('items')).toEqual(
+ expect.arrayContaining([
+ {
+ href: downloadPath,
+ text: `Download ${artifactName}`,
+ extraAttrs: {
+ download: '',
+ rel: 'nofollow',
+ },
+ },
+ ]),
+ );
+ },
+ );
+
+ it.each`
+ artifactName | downloadPath
+ ${'sast-sobelow'} | ${''}
+ ${'sast-pmd-apex'} | ${''}
+ `(
+ 'does not have a dropdown item to download $artifactName artifacts',
+ ({ artifactName, downloadPath }) => {
+ expect(findDropdown().exists()).toBe(true);
+
+ expect(findDropdown().props('items')).not.toEqual(
+ expect.arrayContaining([
+ {
+ href: downloadPath,
+ text: `Download ${artifactName}`,
+ extraAttrs: {
+ download: '',
+ rel: 'nofollow',
+ },
+ },
+ ]),
+ );
},
);
});
diff --git a/spec/frontend/vue_merge_request_widget/extentions/accessibility/index_spec.js b/spec/frontend/vue_merge_request_widget/extentions/accessibility/index_spec.js
index 9b1e694d9c4..baeab1641d2 100644
--- a/spec/frontend/vue_merge_request_widget/extentions/accessibility/index_spec.js
+++ b/spec/frontend/vue_merge_request_widget/extentions/accessibility/index_spec.js
@@ -3,29 +3,25 @@ import { mountExtended } from 'helpers/vue_test_utils_helper';
import { trimText } from 'helpers/text_helper';
import waitForPromises from 'helpers/wait_for_promises';
import axios from '~/lib/utils/axios_utils';
-import extensionsContainer from '~/vue_merge_request_widget/components/extensions/container';
-import { registerExtension } from '~/vue_merge_request_widget/components/extensions';
-import accessibilityExtension from '~/vue_merge_request_widget/extensions/accessibility';
+import AccessibilityWidget from '~/vue_merge_request_widget/extensions/accessibility/index.vue';
import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import { accessibilityReportResponseErrors, accessibilityReportResponseSuccess } from './mock_data';
-describe('Accessibility extension', () => {
+describe('Accessibility widget', () => {
let wrapper;
let mock;
- registerExtension(accessibilityExtension);
-
const endpoint = '/root/repo/-/merge_requests/4/accessibility_reports.json';
const mockApi = (statusCode, data) => {
- mock.onGet(endpoint).reply(statusCode, data);
+ mock.onGet(endpoint).reply(statusCode, data, {});
};
const findToggleCollapsedButton = () => wrapper.findByTestId('toggle-button');
const findAllExtensionListItems = () => wrapper.findAllByTestId('extension-list-item');
const createComponent = () => {
- wrapper = mountExtended(extensionsContainer, {
+ wrapper = mountExtended(AccessibilityWidget, {
propsData: {
mr: {
accessibilityReportPath: endpoint,
diff --git a/spec/frontend/vue_merge_request_widget/mr_widget_options_spec.js b/spec/frontend/vue_merge_request_widget/mr_widget_options_spec.js
index eb3d624dc04..9296e548081 100644
--- a/spec/frontend/vue_merge_request_widget/mr_widget_options_spec.js
+++ b/spec/frontend/vue_merge_request_widget/mr_widget_options_spec.js
@@ -3,13 +3,13 @@ import MockAdapter from 'axios-mock-adapter';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import { createMockSubscription as createMockApolloSubscription } from 'mock-apollo-client';
-import * as Sentry from '@sentry/browser';
import approvedByCurrentUser from 'test_fixtures/graphql/merge_requests/approvals/approvals.query.graphql.json';
import getStateQueryResponse from 'test_fixtures/graphql/merge_requests/get_state.query.graphql.json';
import readyToMergeResponse from 'test_fixtures/graphql/merge_requests/states/ready_to_merge.query.graphql.json';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import api from '~/api';
import axios from '~/lib/utils/axios_utils';
import { HTTP_STATUS_OK, HTTP_STATUS_NO_CONTENT } from '~/lib/utils/http_status';
@@ -63,8 +63,8 @@ jest.mock('~/smart_interval');
jest.mock('~/lib/utils/favicon');
-jest.mock('@sentry/browser', () => ({
- ...jest.requireActual('@sentry/browser'),
+jest.mock('~/sentry/sentry_browser_wrapper', () => ({
+ ...jest.requireActual('~/sentry/sentry_browser_wrapper'),
captureException: jest.fn(),
}));
diff --git a/spec/frontend/vue_shared/components/ci_badge_link_spec.js b/spec/frontend/vue_shared/components/ci_badge_link_spec.js
deleted file mode 100644
index e1660225a5c..00000000000
--- a/spec/frontend/vue_shared/components/ci_badge_link_spec.js
+++ /dev/null
@@ -1,158 +0,0 @@
-import { GlBadge } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
-import CiBadgeLink from '~/vue_shared/components/ci_badge_link.vue';
-import CiIcon from '~/vue_shared/components/ci_icon.vue';
-
-jest.mock('~/lib/utils/url_utility', () => ({
- visitUrl: jest.fn(),
-}));
-
-describe('CI Badge Link Component', () => {
- let wrapper;
-
- const statuses = {
- canceled: {
- text: 'canceled',
- label: 'canceled',
- group: 'canceled',
- icon: 'status_canceled',
- details_path: 'status/canceled',
- },
- created: {
- text: 'created',
- label: 'created',
- group: 'created',
- icon: 'status_created',
- details_path: 'status/created',
- },
- failed: {
- text: 'failed',
- label: 'failed',
- group: 'failed',
- icon: 'status_failed',
- details_path: 'status/failed',
- },
- manual: {
- text: 'manual',
- label: 'manual action',
- group: 'manual',
- icon: 'status_manual',
- details_path: 'status/manual',
- },
- pending: {
- text: 'pending',
- label: 'pending',
- group: 'pending',
- icon: 'status_pending',
- details_path: 'status/pending',
- },
- preparing: {
- text: 'preparing',
- label: 'preparing',
- group: 'preparing',
- icon: 'status_preparing',
- details_path: 'status/preparing',
- },
- running: {
- text: 'running',
- label: 'running',
- group: 'running',
- icon: 'status_running',
- details_path: 'status/running',
- },
- scheduled: {
- text: 'scheduled',
- label: 'scheduled',
- group: 'scheduled',
- icon: 'status_scheduled',
- details_path: 'status/scheduled',
- },
- skipped: {
- text: 'skipped',
- label: 'skipped',
- group: 'skipped',
- icon: 'status_skipped',
- details_path: 'status/skipped',
- },
- success_warining: {
- text: 'warning',
- label: 'passed with warnings',
- group: 'success-with-warnings',
- icon: 'status_warning',
- details_path: 'status/warning',
- },
- success: {
- text: 'passed',
- label: 'passed',
- group: 'passed',
- icon: 'status_success',
- details_path: 'status/passed',
- },
- };
-
- const findIcon = () => wrapper.findComponent(CiIcon);
- const findBadge = () => wrapper.findComponent(GlBadge);
- const findBadgeText = () => wrapper.find('[data-testid="ci-badge-text"');
-
- const createComponent = (propsData) => {
- wrapper = shallowMount(CiBadgeLink, { propsData });
- };
-
- it.each(Object.keys(statuses))('should render badge for status: %s', (status) => {
- createComponent({ status: statuses[status] });
-
- expect(wrapper.attributes('href')).toBe(statuses[status].details_path);
- expect(wrapper.text()).toBe(statuses[status].text);
- expect(findBadge().props('size')).toBe('md');
- expect(findIcon().exists()).toBe(true);
- });
-
- it.each`
- status | textColor | variant
- ${statuses.success} | ${'gl-text-green-700'} | ${'success'}
- ${statuses.success_warining} | ${'gl-text-orange-700'} | ${'warning'}
- ${statuses.failed} | ${'gl-text-red-700'} | ${'danger'}
- ${statuses.running} | ${'gl-text-blue-700'} | ${'info'}
- ${statuses.pending} | ${'gl-text-orange-700'} | ${'warning'}
- ${statuses.preparing} | ${'gl-text-gray-600'} | ${'muted'}
- ${statuses.canceled} | ${'gl-text-gray-700'} | ${'neutral'}
- ${statuses.scheduled} | ${'gl-text-gray-600'} | ${'muted'}
- ${statuses.skipped} | ${'gl-text-gray-600'} | ${'muted'}
- ${statuses.manual} | ${'gl-text-gray-700'} | ${'neutral'}
- ${statuses.created} | ${'gl-text-gray-600'} | ${'muted'}
- `(
- 'should contain correct badge class and variant for status: $status.text',
- ({ status, textColor, variant }) => {
- createComponent({ status });
-
- expect(findBadgeText().classes()).toContain(textColor);
- expect(findBadge().props('variant')).toBe(variant);
- },
- );
-
- it('should not render label', () => {
- createComponent({ status: statuses.canceled, showText: false });
-
- expect(wrapper.text()).toBe('');
- });
-
- it('should emit ciStatusBadgeClick event', () => {
- createComponent({ status: statuses.success });
-
- findBadge().vm.$emit('click');
-
- expect(wrapper.emitted('ciStatusBadgeClick')).toEqual([[]]);
- });
-
- it('should render dynamic badge size', () => {
- createComponent({ status: statuses.success, size: 'lg' });
-
- expect(findBadge().props('size')).toBe('lg');
- });
-
- it('should have class `gl-px-2` when `showText` is false', () => {
- createComponent({ status: statuses.success, size: 'md', showText: false });
-
- expect(findBadge().classes()).toContain('gl-px-2');
- });
-});
diff --git a/spec/frontend/vue_shared/components/ci_icon_spec.js b/spec/frontend/vue_shared/components/ci_icon_spec.js
index c907b776b91..cbb725bf9e6 100644
--- a/spec/frontend/vue_shared/components/ci_icon_spec.js
+++ b/spec/frontend/vue_shared/components/ci_icon_spec.js
@@ -2,92 +2,135 @@ import { GlIcon } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import CiIcon from '~/vue_shared/components/ci_icon.vue';
+const mockStatus = {
+ group: 'success',
+ icon: 'status_success',
+ text: 'Success',
+};
+
describe('CI Icon component', () => {
let wrapper;
- const createComponent = (props) => {
+ const createComponent = ({ props = {} } = {}) => {
wrapper = shallowMount(CiIcon, {
propsData: {
+ status: mockStatus,
...props,
},
});
};
- it('should render a span element with an svg', () => {
- createComponent({
- status: {
- group: 'success',
- icon: 'status_success',
- },
- });
+ const findIcon = () => wrapper.findComponent(GlIcon);
- expect(wrapper.find('span').exists()).toBe(true);
- expect(wrapper.findComponent(GlIcon).exists()).toBe(true);
+ it('should render a span element and an icon', () => {
+ createComponent();
+
+ expect(wrapper.attributes('size')).toBe('md');
+ expect(findIcon().exists()).toBe(true);
});
describe.each`
- isActive
- ${true}
- ${false}
- `('when isActive is $isActive', ({ isActive }) => {
- it(`"active" class is ${isActive ? 'not ' : ''}added`, () => {
- wrapper = shallowMount(CiIcon, {
- propsData: {
+ showStatusText | showTooltip | expectedText | expectedTooltip | expectedAriaLabel
+ ${true} | ${true} | ${'Success'} | ${undefined} | ${undefined}
+ ${true} | ${false} | ${'Success'} | ${undefined} | ${undefined}
+ ${false} | ${true} | ${''} | ${'Success'} | ${'Success'}
+ ${false} | ${false} | ${''} | ${undefined} | ${'Success'}
+ `(
+ 'when showStatusText is %{showStatusText} and showTooltip is %{showTooltip}',
+ ({ showStatusText, showTooltip, expectedText, expectedTooltip, expectedAriaLabel }) => {
+ beforeEach(() => {
+ createComponent({
+ props: {
+ showStatusText,
+ showTooltip,
+ },
+ });
+ });
+
+ it(`aria-label is ${expectedAriaLabel}`, () => {
+ expect(wrapper.attributes('aria-label')).toBe(expectedAriaLabel);
+ });
+
+ it(`text shown is ${expectedAriaLabel}`, () => {
+ expect(wrapper.text()).toBe(expectedText);
+ });
+
+ it(`tooltip shown is ${expectedAriaLabel}`, () => {
+ expect(wrapper.attributes('title')).toBe(expectedTooltip);
+ });
+ },
+ );
+
+ describe('when appearing as a link', () => {
+ it('shows a GraphQL path', () => {
+ createComponent({
+ props: {
status: {
- group: 'success',
- icon: 'status_success',
+ ...mockStatus,
+ detailsPath: '/path',
},
- isActive,
+ useLink: true,
},
});
- expect(wrapper.classes('active')).toBe(isActive);
+ expect(wrapper.attributes('href')).toBe('/path');
});
- });
- describe.each`
- isInteractive
- ${true}
- ${false}
- `('when isInteractive is $isInteractive', ({ isInteractive }) => {
- it(`"interactive" class is ${isInteractive ? 'not ' : ''}added`, () => {
- wrapper = shallowMount(CiIcon, {
- propsData: {
+ it('shows a REST API path', () => {
+ createComponent({
+ props: {
status: {
- group: 'success',
- icon: 'status_success',
+ ...mockStatus,
+ details_path: '/path',
},
- isInteractive,
+ useLink: true,
+ },
+ });
+
+ expect(wrapper.attributes('href')).toBe('/path');
+ });
+
+ it('shows no path', () => {
+ createComponent({
+ status: {
+ detailsPath: '/path',
+ details_path: '/path',
+ },
+ props: {
+ useLink: false,
},
});
- expect(wrapper.classes('interactive')).toBe(isInteractive);
+ expect(wrapper.attributes('href')).toBe(undefined);
});
});
- describe('rendering a status', () => {
+ describe('rendering a status icon and class', () => {
it.each`
- icon | group | cssClass
- ${'status_success'} | ${'success'} | ${'ci-status-icon-success'}
- ${'status_failed'} | ${'failed'} | ${'ci-status-icon-failed'}
- ${'status_warning'} | ${'warning'} | ${'ci-status-icon-warning'}
- ${'status_pending'} | ${'pending'} | ${'ci-status-icon-pending'}
- ${'status_running'} | ${'running'} | ${'ci-status-icon-running'}
- ${'status_created'} | ${'created'} | ${'ci-status-icon-created'}
- ${'status_skipped'} | ${'skipped'} | ${'ci-status-icon-skipped'}
- ${'status_canceled'} | ${'canceled'} | ${'ci-status-icon-canceled'}
- ${'status_manual'} | ${'manual'} | ${'ci-status-icon-manual'}
- `('should render a $group status', ({ icon, group, cssClass }) => {
- wrapper = shallowMount(CiIcon, {
- propsData: {
+ icon | variant
+ ${'status_success'} | ${'success'}
+ ${'status_warning'} | ${'warning'}
+ ${'status_pending'} | ${'warning'}
+ ${'status_failed'} | ${'danger'}
+ ${'status_running'} | ${'info'}
+ ${'status_created'} | ${'neutral'}
+ ${'status_skipped'} | ${'neutral'}
+ ${'status_canceled'} | ${'neutral'}
+ ${'status_manual'} | ${'neutral'}
+ `('should render a $group status', ({ icon, variant }) => {
+ createComponent({
+ props: {
status: {
+ ...mockStatus,
icon,
- group,
},
+ showStatusText: true,
},
});
+ expect(wrapper.attributes('variant')).toBe(variant);
+ expect(wrapper.classes(`ci-icon-variant-${variant}`)).toBe(true);
- expect(wrapper.classes()).toContain(cssClass);
+ expect(findIcon().props('name')).toBe(`${icon}_borderless`);
});
});
});
diff --git a/spec/frontend/vue_shared/components/diff_stats_dropdown_spec.js b/spec/frontend/vue_shared/components/diff_stats_dropdown_spec.js
index 40232eb367a..810269257b6 100644
--- a/spec/frontend/vue_shared/components/diff_stats_dropdown_spec.js
+++ b/spec/frontend/vue_shared/components/diff_stats_dropdown_spec.js
@@ -1,15 +1,16 @@
import {
+ GlDisclosureDropdown,
+ GlDisclosureDropdownItem,
GlSprintf,
- GlDropdown,
- GlDropdownItem,
- GlDropdownText,
GlSearchBoxByType,
+ GlIcon,
} from '@gitlab/ui';
import fuzzaldrinPlus from 'fuzzaldrin-plus';
import { nextTick } from 'vue';
-import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
import { stubComponent } from 'helpers/stub_component';
import DiffStatsDropdown, { i18n } from '~/vue_shared/components/diff_stats_dropdown.vue';
+import { ARROW_DOWN_KEY } from '~/lib/utils/keys';
jest.mock('fuzzaldrin-plus', () => ({
filter: jest.fn().mockReturnValue([]),
@@ -42,7 +43,7 @@ describe('Diff Stats Dropdown', () => {
const focusInputMock = jest.fn();
const createComponent = ({ changed = 0, added = 0, deleted = 0, files = [] } = {}) => {
- wrapper = shallowMountExtended(DiffStatsDropdown, {
+ wrapper = mountExtended(DiffStatsDropdown, {
propsData: {
changed,
added,
@@ -51,7 +52,6 @@ describe('Diff Stats Dropdown', () => {
},
stubs: {
GlSprintf,
- GlDropdown,
GlSearchBoxByType: stubComponent(GlSearchBoxByType, {
methods: { focusInput: focusInputMock },
}),
@@ -59,9 +59,8 @@ describe('Diff Stats Dropdown', () => {
});
};
- const findChanged = () => wrapper.findComponent(GlDropdown);
- const findChangedFiles = () => findChanged().findAllComponents(GlDropdownItem);
- const findNoFilesText = () => findChanged().findComponent(GlDropdownText);
+ const findChanged = () => wrapper.findComponent(GlDisclosureDropdown);
+ const findChangedFiles = () => findChanged().findAllComponents(GlDisclosureDropdownItem);
const findCollapsed = () => wrapper.findByTestId('diff-stats-additions-deletions-expanded');
const findSearchBox = () => wrapper.findComponent(GlSearchBoxByType);
@@ -79,15 +78,14 @@ describe('Diff Stats Dropdown', () => {
const fileText = findChangedFiles().at(1).text();
expect(fileText).toContain(mockFiles[1].name);
expect(fileText).toContain(mockFiles[1].path);
- expect(fileData.props()).toMatchObject({
- iconName: mockFiles[1].icon,
- iconColor: mockFiles[1].iconColor,
- });
+ expect(fileData.findComponent(GlIcon).props('name')).toEqual(mockFiles[1].icon);
+ expect(fileData.findComponent(GlIcon).classes()).toContain('gl-text-red-500');
+ expect(fileData.find('a').attributes('href')).toEqual(mockFiles[1].href);
});
it('when no files changed', () => {
createComponent({ files: [] });
- expect(findNoFilesText().text()).toContain(i18n.noFilesFound);
+ expect(findChanged().text()).toContain(i18n.noFilesFound);
});
});
@@ -108,7 +106,7 @@ describe('Diff Stats Dropdown', () => {
});
it(`dropdown header should be '${expectedDropdownHeader}'`, () => {
- expect(findChanged().props('text')).toBe(expectedDropdownHeader);
+ expect(findChanged().props('toggleText')).toBe(expectedDropdownHeader);
});
it(`added and deleted count in collapsed section should be '${expectedAddedDeletedCollapsed}'`, () => {
@@ -137,27 +135,27 @@ describe('Diff Stats Dropdown', () => {
});
});
- describe('selecting file dropdown item', () => {
+ describe('on dropdown open', () => {
beforeEach(() => {
- createComponent({ files: mockFiles });
+ createComponent();
});
- it('updates the URL', () => {
- findChangedFiles().at(0).vm.$emit('click');
- expect(window.location.hash).toBe(mockFiles[0].href);
- findChangedFiles().at(1).vm.$emit('click');
- expect(window.location.hash).toBe(mockFiles[1].href);
+ it('should set the search input focus', () => {
+ findChanged().vm.$emit('shown');
+ expect(focusInputMock).toHaveBeenCalled();
});
});
- describe('on dropdown open', () => {
+ describe('keyboard nav', () => {
beforeEach(() => {
- createComponent();
+ createComponent({ files: mockFiles });
});
- it('should set the search input focus', () => {
- findChanged().vm.$emit('shown');
- expect(focusInputMock).toHaveBeenCalled();
+ it('focuses the first item when pressing the down key within the search box', () => {
+ const spy = jest.spyOn(wrapper.vm, 'focusFirstItem');
+ findSearchBox().vm.$emit('keydown', new KeyboardEvent({ key: ARROW_DOWN_KEY }));
+
+ expect(spy).toHaveBeenCalled();
});
});
});
diff --git a/spec/frontend/vue_shared/components/ensure_data_spec.js b/spec/frontend/vue_shared/components/ensure_data_spec.js
index 217e795bc64..399fe19ea3f 100644
--- a/spec/frontend/vue_shared/components/ensure_data_spec.js
+++ b/spec/frontend/vue_shared/components/ensure_data_spec.js
@@ -1,6 +1,6 @@
import { GlEmptyState } from '@gitlab/ui';
-import * as Sentry from '@sentry/browser';
import { mount } from '@vue/test-utils';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import ensureData from '~/ensure_data';
const mockData = { message: 'Hello there' };
diff --git a/spec/frontend/vue_shared/components/entity_select/entity_select_spec.js b/spec/frontend/vue_shared/components/entity_select/entity_select_spec.js
index 36772ad03fe..1376133ec37 100644
--- a/spec/frontend/vue_shared/components/entity_select/entity_select_spec.js
+++ b/spec/frontend/vue_shared/components/entity_select/entity_select_spec.js
@@ -23,10 +23,13 @@ describe('EntitySelect', () => {
// Props
const label = 'label';
+ const description = 'description';
const inputName = 'inputName';
const inputId = 'inputId';
const headerText = 'headerText';
const defaultToggleText = 'defaultToggleText';
+ const toggleClass = 'foo-bar';
+ const block = true;
// Finders
const findListbox = () => wrapper.findComponent(GlCollapsibleListbox);
@@ -37,11 +40,14 @@ describe('EntitySelect', () => {
wrapper = shallowMountExtended(EntitySelect, {
propsData: {
label,
+ description,
inputName,
inputId,
headerText,
defaultToggleText,
fetchItems: fetchItemsMock,
+ toggleClass,
+ block,
...props,
},
stubs: {
@@ -65,6 +71,21 @@ describe('EntitySelect', () => {
fetchItemsMock = jest.fn().mockImplementation(() => ({ items: [itemMock], totalPages: 1 }));
});
+ describe('GlCollapsibleListbox props', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it.each`
+ prop | expectedValue
+ ${'block'} | ${block}
+ ${'toggleClass'} | ${toggleClass}
+ ${'headerText'} | ${headerText}
+ `('passes the $prop prop to GlCollapsibleListbox', ({ prop, expectedValue }) => {
+ expect(findListbox().props(prop)).toBe(expectedValue);
+ });
+ });
+
describe('on mount', () => {
it('calls the fetch function when the listbox is opened', async () => {
createComponent();
@@ -114,6 +135,12 @@ describe('EntitySelect', () => {
expect(wrapper.findByTestId(testid).exists()).toBe(true);
});
+ it('passes description prop to GlFormGroup', () => {
+ createComponent();
+
+ expect(wrapper.findComponent(GlFormGroup).attributes('description')).toBe(description);
+ });
+
describe('selection', () => {
it('uses the default toggle text while no group is selected', () => {
createComponent();
diff --git a/spec/frontend/vue_shared/components/entity_select/organization_select_spec.js b/spec/frontend/vue_shared/components/entity_select/organization_select_spec.js
new file mode 100644
index 00000000000..ea029ba4f27
--- /dev/null
+++ b/spec/frontend/vue_shared/components/entity_select/organization_select_spec.js
@@ -0,0 +1,179 @@
+import VueApollo from 'vue-apollo';
+import Vue, { nextTick } from 'vue';
+import { GlCollapsibleListbox } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import OrganizationSelect from '~/vue_shared/components/entity_select/organization_select.vue';
+import EntitySelect from '~/vue_shared/components/entity_select/entity_select.vue';
+import {
+ ORGANIZATION_TOGGLE_TEXT,
+ ORGANIZATION_HEADER_TEXT,
+ FETCH_ORGANIZATIONS_ERROR,
+ FETCH_ORGANIZATION_ERROR,
+} from '~/vue_shared/components/entity_select/constants';
+import resolvers from '~/organizations/shared/graphql/resolvers';
+import organizationsQuery from '~/organizations/index/graphql/organizations.query.graphql';
+import { organizations as organizationsMock } from '~/organizations/mock_data';
+import waitForPromises from 'helpers/wait_for_promises';
+import createMockApollo from 'helpers/mock_apollo_helper';
+
+Vue.use(VueApollo);
+
+jest.useFakeTimers();
+
+describe('OrganizationSelect', () => {
+ let wrapper;
+ let mockApollo;
+
+ // Mocks
+ const [organizationMock] = organizationsMock;
+
+ // Stubs
+ const GlAlert = {
+ template: '<div><slot /></div>',
+ };
+
+ // Props
+ const label = 'label';
+ const description = 'description';
+ const inputName = 'inputName';
+ const inputId = 'inputId';
+ const toggleClass = 'foo-bar';
+
+ // Finders
+ const findListbox = () => wrapper.findComponent(GlCollapsibleListbox);
+ const findEntitySelect = () => wrapper.findComponent(EntitySelect);
+ const findAlert = () => wrapper.findComponent(GlAlert);
+
+ const handleInput = jest.fn();
+
+ // Helpers
+ const createComponent = ({ props = {}, mockResolvers = resolvers, handlers } = {}) => {
+ mockApollo = createMockApollo(
+ handlers || [
+ [
+ organizationsQuery,
+ jest.fn().mockResolvedValueOnce({
+ data: { currentUser: { id: 1, organizations: { nodes: organizationsMock } } },
+ }),
+ ],
+ ],
+ mockResolvers,
+ );
+
+ wrapper = shallowMountExtended(OrganizationSelect, {
+ apolloProvider: mockApollo,
+ propsData: {
+ label,
+ description,
+ inputName,
+ inputId,
+ toggleClass,
+ ...props,
+ },
+ stubs: {
+ GlAlert,
+ EntitySelect,
+ },
+ listeners: {
+ input: handleInput,
+ },
+ });
+ };
+ const openListbox = () => findListbox().vm.$emit('shown');
+
+ afterEach(() => {
+ mockApollo = null;
+ });
+
+ describe('entity_select props', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it.each`
+ prop | expectedValue
+ ${'label'} | ${label}
+ ${'description'} | ${description}
+ ${'inputName'} | ${inputName}
+ ${'inputId'} | ${inputId}
+ ${'defaultToggleText'} | ${ORGANIZATION_TOGGLE_TEXT}
+ ${'headerText'} | ${ORGANIZATION_HEADER_TEXT}
+ ${'toggleClass'} | ${toggleClass}
+ `('passes the $prop prop to entity-select', ({ prop, expectedValue }) => {
+ expect(findEntitySelect().props(prop)).toBe(expectedValue);
+ });
+ });
+
+ describe('on mount', () => {
+ it('fetches organizations when the listbox is opened', async () => {
+ createComponent();
+ await nextTick();
+ jest.runAllTimers();
+ await waitForPromises();
+
+ openListbox();
+ jest.runAllTimers();
+ await waitForPromises();
+ expect(findListbox().props('items')).toEqual([
+ { text: organizationsMock[0].name, value: 1 },
+ { text: organizationsMock[1].name, value: 2 },
+ { text: organizationsMock[2].name, value: 3 },
+ ]);
+ });
+
+ describe('with an initial selection', () => {
+ it("fetches the initially selected value's name", async () => {
+ createComponent({ props: { initialSelection: organizationMock.id } });
+ await nextTick();
+ jest.runAllTimers();
+ await waitForPromises();
+
+ expect(findListbox().props('toggleText')).toBe(organizationMock.name);
+ });
+
+ it('show an error if fetching initially selected fails', async () => {
+ const mockResolvers = {
+ Query: {
+ organization: jest.fn().mockRejectedValueOnce(new Error()),
+ },
+ };
+
+ createComponent({ props: { initialSelection: organizationMock.id }, mockResolvers });
+ await nextTick();
+ jest.runAllTimers();
+
+ expect(findAlert().exists()).toBe(false);
+
+ await waitForPromises();
+
+ expect(findAlert().exists()).toBe(true);
+ expect(findAlert().text()).toBe(FETCH_ORGANIZATION_ERROR);
+ });
+ });
+ });
+
+ it('shows an error when fetching organizations fails', async () => {
+ createComponent({
+ handlers: [[organizationsQuery, jest.fn().mockRejectedValueOnce(new Error())]],
+ });
+ await nextTick();
+ jest.runAllTimers();
+ await waitForPromises();
+
+ openListbox();
+ expect(findAlert().exists()).toBe(false);
+
+ jest.runAllTimers();
+ await waitForPromises();
+
+ expect(findAlert().exists()).toBe(true);
+ expect(findAlert().text()).toBe(FETCH_ORGANIZATIONS_ERROR);
+ });
+
+ it('forwards events to the parent scope via `v-on="$listeners"`', () => {
+ createComponent();
+ findEntitySelect().vm.$emit('input');
+
+ expect(handleInput).toHaveBeenCalledTimes(1);
+ });
+});
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/filtered_search_bar_root_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/filtered_search_bar_root_spec.js
index 00a412d9de8..bb612a13209 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
@@ -297,27 +297,31 @@ describe('FilteredSearchBarRoot', () => {
await nextTick();
});
- it('calls `uniqueTokens` on `filterValue` prop to remove duplicates', () => {
- wrapper.vm.handleFilterSubmit();
+ it('calls `uniqueTokens` on `filterValue` prop to remove duplicates', async () => {
+ findGlFilteredSearch().vm.$emit('submit');
+ await nextTick();
expect(uniqueTokens).toHaveBeenCalledWith(wrapper.vm.filterValue);
});
- it('calls `recentSearchesStore.addRecentSearch` with serialized value of provided `filters` param', () => {
+ it('calls `recentSearchesStore.addRecentSearch` with serialized value of provided `filters` param', async () => {
jest.spyOn(wrapper.vm.recentSearchesStore, 'addRecentSearch');
- wrapper.vm.handleFilterSubmit();
+ findGlFilteredSearch().vm.$emit('submit');
+ await nextTick();
return wrapper.vm.recentSearchesPromise.then(() => {
expect(wrapper.vm.recentSearchesStore.addRecentSearch).toHaveBeenCalledWith(mockFilters);
});
});
- it('calls `recentSearchesService.save` with array of searches', () => {
+ it('calls `recentSearchesService.save` with array of searches', async () => {
jest.spyOn(wrapper.vm.recentSearchesService, 'save');
wrapper.vm.handleFilterSubmit();
+ await nextTick();
+
return wrapper.vm.recentSearchesPromise.then(() => {
expect(wrapper.vm.recentSearchesService.save).toHaveBeenCalledWith([mockFilters]);
});
@@ -336,15 +340,16 @@ describe('FilteredSearchBarRoot', () => {
it('calls `blurSearchInput` method to remove focus from filter input field', () => {
jest.spyOn(wrapper.vm, 'blurSearchInput');
- wrapper.findComponent(GlFilteredSearch).vm.$emit('submit', mockFilters);
+ findGlFilteredSearch().vm.$emit('submit', mockFilters);
expect(wrapper.vm.blurSearchInput).toHaveBeenCalled();
});
- it('emits component event `onFilter` with provided filters param', () => {
+ it('emits component event `onFilter` with provided filters param', async () => {
jest.spyOn(wrapper.vm, 'removeQuotesEnclosure');
- wrapper.vm.handleFilterSubmit();
+ findGlFilteredSearch().vm.$emit('submit');
+ await nextTick();
expect(wrapper.emitted('onFilter')[0]).toEqual([mockFilters]);
expect(wrapper.vm.removeQuotesEnclosure).toHaveBeenCalledWith(mockFilters);
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/base_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/base_token_spec.js
index 72e3475df75..88618de6979 100644
--- a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/base_token_spec.js
+++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/base_token_spec.js
@@ -88,6 +88,7 @@ function createComponent({
slots = defaultSlots,
scopedSlots = defaultScopedSlots,
mountFn = mount,
+ groupMultiSelectTokens = false,
} = {}) {
return mountFn(BaseToken, {
propsData: {
@@ -95,6 +96,9 @@ function createComponent({
...props,
},
provide: {
+ glFeatures: {
+ groupMultiSelectTokens,
+ },
portalName: 'fake target',
alignSuggestions: jest.fn(),
suggestionsListClass: () => 'custom-class',
@@ -148,6 +152,24 @@ describe('BaseToken', () => {
`"${mockRegularLabel.title}"`,
);
});
+
+ it('uses last item in list when value is an array', () => {
+ const mockGetActiveTokenValue = jest.fn();
+
+ wrapper = createComponent({
+ props: {
+ value: { data: mockLabels.map((l) => l.title) },
+ suggestions: mockLabels,
+ getActiveTokenValue: mockGetActiveTokenValue,
+ },
+ groupMultiSelectTokens: true,
+ });
+
+ const lastTitle = mockLabels[mockLabels.length - 1].title;
+
+ expect(mockGetActiveTokenValue).toHaveBeenCalledTimes(1);
+ expect(mockGetActiveTokenValue).toHaveBeenCalledWith(mockLabels, lastTitle);
+ });
});
});
@@ -385,6 +407,28 @@ describe('BaseToken', () => {
expect(setTokenValueToRecentlyUsed).not.toHaveBeenCalled();
});
+
+ it('emits token-selected event when groupMultiSelectTokens: true', () => {
+ wrapper = createComponent({
+ props: { suggestions: mockLabels },
+ groupMultiSelectTokens: true,
+ });
+
+ findGlFilteredSearchToken().vm.$emit('select', mockTokenValue.title);
+
+ expect(wrapper.emitted('token-selected')).toEqual([[mockTokenValue.title]]);
+ });
+
+ it('does not emit token-selected event when groupMultiSelectTokens: true', () => {
+ wrapper = createComponent({
+ props: { suggestions: mockLabels },
+ groupMultiSelectTokens: false,
+ });
+
+ findGlFilteredSearchToken().vm.$emit('select', mockTokenValue.title);
+
+ expect(wrapper.emitted('token-selected')).toBeUndefined();
+ });
});
});
@@ -476,6 +520,14 @@ describe('BaseToken', () => {
expect(wrapper.emitted('fetch-suggestions')[2]).toEqual(['foo']);
});
});
+
+ it('does not emit `fetch-suggestions` when value is array', () => {
+ expect(wrapper.emitted('fetch-suggestions')).toEqual([[''], ['']]);
+
+ findGlFilteredSearchToken().vm.$emit('input', { data: ['first item'] });
+
+ expect(wrapper.emitted('fetch-suggestions')).toEqual([[''], ['']]);
+ });
});
});
});
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/user_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/user_token_spec.js
index 0229d00eb91..4462d1bfaf5 100644
--- a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/user_token_spec.js
+++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/user_token_spec.js
@@ -3,6 +3,7 @@ import {
GlFilteredSearchSuggestion,
GlDropdownDivider,
GlAvatar,
+ GlIcon,
} from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
@@ -53,6 +54,7 @@ function createComponent(options = {}) {
stubs = defaultStubs,
data = {},
listeners = {},
+ groupMultiSelectTokens = false,
} = options;
return mount(UserToken, {
apolloProvider: mockApollo,
@@ -63,6 +65,9 @@ function createComponent(options = {}) {
cursorPosition: 'start',
},
provide: {
+ glFeatures: {
+ groupMultiSelectTokens,
+ },
portalName: 'fake target',
alignSuggestions: function fakeAlignSuggestions() {},
suggestionsListClass: () => 'custom-class',
@@ -82,6 +87,8 @@ describe('UserToken', () => {
let wrapper;
const findBaseToken = () => wrapper.findComponent(BaseToken);
+ const findSuggestions = () => wrapper.findAllComponents(GlFilteredSearchSuggestion);
+ const findIconAtSuggestion = (index) => findSuggestions().at(index).findComponent(GlIcon);
beforeEach(() => {
mock = new MockAdapter(axios);
@@ -303,6 +310,110 @@ describe('UserToken', () => {
expect(mockInput).toHaveBeenLastCalledWith([{ data: 'mockData', operator: '=' }]);
});
+ describe('multiSelect', () => {
+ it('renders check icons in suggestions when multiSelect is true', async () => {
+ wrapper = createComponent({
+ value: { data: [mockUsers[0].username, mockUsers[1].username], operator: '=' },
+ data: {
+ users: mockUsers,
+ },
+ config: { ...mockAuthorToken, multiSelect: true, initialUsers: mockUsers },
+ active: true,
+ stubs: { Portal: true },
+ groupMultiSelectTokens: true,
+ });
+
+ await activateSuggestionsList();
+
+ const suggestions = wrapper.findAllComponents(GlFilteredSearchSuggestion);
+
+ expect(findIconAtSuggestion(1).exists()).toBe(false);
+ expect(findIconAtSuggestion(2).props('name')).toBe('check');
+ expect(findIconAtSuggestion(3).props('name')).toBe('check');
+
+ // test for left padding on unchecked items (so alignment is correct)
+ expect(findIconAtSuggestion(4).exists()).toBe(false);
+ expect(suggestions.at(4).find('.gl-pl-6').exists()).toBe(true);
+ });
+
+ it('renders multiple users when multiSelect is true', async () => {
+ wrapper = createComponent({
+ value: { data: [mockUsers[0].username, mockUsers[1].username], operator: '=' },
+ data: {
+ users: mockUsers,
+ },
+ config: { ...mockAuthorToken, multiSelect: true, initialUsers: mockUsers },
+ groupMultiSelectTokens: true,
+ });
+
+ await nextTick();
+ const tokenSegments = wrapper.findAllComponents(GlFilteredSearchTokenSegment);
+
+ expect(tokenSegments).toHaveLength(3); // Author, =, "Administrator"
+
+ const tokenValue = tokenSegments.at(2);
+
+ const [user1, user2] = mockUsers;
+
+ expect(tokenValue.findAllComponents(GlAvatar).at(1).props('src')).toBe(
+ mockUsers[1].avatar_url,
+ );
+ expect(tokenValue.text()).toBe(`${user1.name},${user2.name}`);
+ });
+
+ it('adds new user to multi-select-values', () => {
+ wrapper = createComponent({
+ value: { data: [mockUsers[0].username], operator: '=' },
+ data: {
+ users: mockUsers,
+ },
+ config: { ...mockAuthorToken, multiSelect: true, initialUsers: mockUsers },
+ active: true,
+ groupMultiSelectTokens: true,
+ });
+
+ findBaseToken().vm.$emit('token-selected', mockUsers[1].username);
+
+ expect(findBaseToken().props().multiSelectValues).toEqual([
+ mockUsers[0].username,
+ mockUsers[1].username,
+ ]);
+ });
+
+ it('removes existing user from array', () => {
+ const initialUsers = [mockUsers[0].username, mockUsers[1].username];
+ wrapper = createComponent({
+ value: { data: initialUsers, operator: '=' },
+ data: {
+ users: mockUsers,
+ },
+ config: { ...mockAuthorToken, multiSelect: true, initialUsers: mockUsers },
+ active: true,
+ groupMultiSelectTokens: true,
+ });
+
+ findBaseToken().vm.$emit('token-selected', mockUsers[0].username);
+
+ expect(findBaseToken().props().multiSelectValues).toEqual([mockUsers[1].username]);
+ });
+
+ it('clears input field after token selected', () => {
+ wrapper = createComponent({
+ value: { data: [mockUsers[0].username, mockUsers[1].username], operator: '=' },
+ data: {
+ users: mockUsers,
+ },
+ config: { ...mockAuthorToken, multiSelect: true, initialUsers: mockUsers },
+ active: true,
+ groupMultiSelectTokens: true,
+ });
+
+ findBaseToken().vm.$emit('token-selected', 'test');
+
+ expect(wrapper.emitted('input')).toEqual([[{ operator: '=', data: '' }]]);
+ });
+ });
+
describe('when loading', () => {
beforeEach(() => {
wrapper = createComponent({
diff --git a/spec/frontend/vue_shared/components/form/errors_alert_spec.js b/spec/frontend/vue_shared/components/form/errors_alert_spec.js
new file mode 100644
index 00000000000..b7efe19378d
--- /dev/null
+++ b/spec/frontend/vue_shared/components/form/errors_alert_spec.js
@@ -0,0 +1,60 @@
+import { GlAlert } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import FormErrorsAlert from '~/vue_shared/components/form/errors_alert.vue';
+
+describe('FormErrorsAlert', () => {
+ let wrapper;
+
+ const defaultPropsData = {
+ errors: ['Foo', 'Bar', 'Baz'],
+ };
+
+ function createComponent({ propsData = {} } = {}) {
+ wrapper = shallowMount(FormErrorsAlert, {
+ propsData: {
+ ...defaultPropsData,
+ ...propsData,
+ },
+ });
+ }
+
+ const findAlert = () => wrapper.findComponent(GlAlert);
+
+ describe('when there are no errors', () => {
+ it('renders nothing', () => {
+ createComponent({ propsData: { errors: [] } });
+
+ expect(wrapper.html()).toBe('');
+ });
+ });
+
+ describe('when there is one error', () => {
+ it('renders correct title and message', () => {
+ createComponent({ propsData: { errors: ['Foo'] } });
+
+ expect(findAlert().props('title')).toBe('The form contains the following error:');
+ expect(findAlert().text()).toContain('Foo');
+ });
+ });
+
+ describe('when there are multiple errors', () => {
+ it('renders correct title and message', () => {
+ createComponent();
+
+ expect(findAlert().props('title')).toBe('The form contains the following errors:');
+ expect(findAlert().text()).toContain('Foo');
+ expect(findAlert().text()).toContain('Bar');
+ expect(findAlert().text()).toContain('Baz');
+ });
+ });
+
+ describe('when alert is dismissed', () => {
+ it('emits input event with empty array as payload', () => {
+ createComponent();
+
+ findAlert().vm.$emit('dismiss');
+
+ expect(wrapper.emitted('input')).toEqual([[[]]]);
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/form/input_copy_toggle_visibility_spec.js b/spec/frontend/vue_shared/components/form/input_copy_toggle_visibility_spec.js
index 72a0eb98a07..b57643a1359 100644
--- a/spec/frontend/vue_shared/components/form/input_copy_toggle_visibility_spec.js
+++ b/spec/frontend/vue_shared/components/form/input_copy_toggle_visibility_spec.js
@@ -44,11 +44,11 @@ describe('InputCopyToggleVisibility', () => {
};
function expectInputToBeMasked() {
- expect(findFormInput().element.type).toBe('password');
+ expect(findFormInput().classes()).toContain('input-copy-show-disc');
}
function expectInputToBeRevealed() {
- expect(findFormInput().element.type).toBe('text');
+ expect(findFormInput().classes()).not.toContain('input-copy-show-disc');
expect(findFormInput().element.value).toBe(valueProp);
}
diff --git a/spec/frontend/vue_shared/components/list_selector/group_item_spec.js b/spec/frontend/vue_shared/components/list_selector/group_item_spec.js
new file mode 100644
index 00000000000..b59e4c734c1
--- /dev/null
+++ b/spec/frontend/vue_shared/components/list_selector/group_item_spec.js
@@ -0,0 +1,55 @@
+import { GlAvatar } from '@gitlab/ui';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import GroupItem from '~/vue_shared/components/list_selector/group_item.vue';
+
+describe('GroupItem spec', () => {
+ let wrapper;
+
+ const MOCK_GROUP = { fullName: 'Group 1', name: 'group1', avatarUrl: 'some/avatar.jpg' };
+
+ const createComponent = (props) => {
+ wrapper = mountExtended(GroupItem, {
+ propsData: {
+ data: MOCK_GROUP,
+ ...props,
+ },
+ });
+ };
+
+ const findAvatar = () => wrapper.findComponent(GlAvatar);
+ const findDeleteButton = () => wrapper.findByRole('button', { fullName: 'Delete Group 1' });
+
+ beforeEach(() => createComponent());
+
+ it('renders an Avatar component', () => {
+ expect(findAvatar().props('size')).toBe(32);
+ expect(findAvatar().attributes()).toMatchObject({
+ src: MOCK_GROUP.avatarUrl,
+ alt: MOCK_GROUP.fullName,
+ });
+ });
+
+ it('renders a fullName and name', () => {
+ expect(wrapper.text()).toContain('Group 1');
+ expect(wrapper.text()).toContain('@group1');
+ });
+
+ it('does not render a delete button by default', () => {
+ expect(findDeleteButton().exists()).toBe(false);
+ });
+
+ describe('Delete button', () => {
+ beforeEach(() => createComponent({ canDelete: true }));
+
+ it('renders a delete button', () => {
+ expect(findDeleteButton().exists()).toBe(true);
+ expect(findDeleteButton().props('icon')).toBe('remove');
+ });
+
+ it('emits a delete event if the delete button is clicked', () => {
+ findDeleteButton().trigger('click');
+
+ expect(wrapper.emitted('delete')).toEqual([[MOCK_GROUP.name]]);
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/list_selector/index_spec.js b/spec/frontend/vue_shared/components/list_selector/index_spec.js
new file mode 100644
index 00000000000..11e64a91eb0
--- /dev/null
+++ b/spec/frontend/vue_shared/components/list_selector/index_spec.js
@@ -0,0 +1,257 @@
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import { GlCard, GlIcon, GlCollapsibleListbox, GlSearchBoxByType } from '@gitlab/ui';
+import Api from '~/api';
+import { createAlert } from '~/alert';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import ListSelector from '~/vue_shared/components/list_selector/index.vue';
+import UserItem from '~/vue_shared/components/list_selector/user_item.vue';
+import GroupItem from '~/vue_shared/components/list_selector/group_item.vue';
+import groupsAutocompleteQuery from '~/graphql_shared/queries/groups_autocomplete.query.graphql';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import { USERS_RESPONSE_MOCK, GROUPS_RESPONSE_MOCK } from './mock_data';
+
+jest.mock('~/alert');
+Vue.use(VueApollo);
+
+describe('List Selector spec', () => {
+ let wrapper;
+ let fakeApollo;
+
+ const USERS_MOCK_PROPS = {
+ title: 'Users',
+ projectPath: 'some/project/path',
+ groupPath: 'some/group/path',
+ type: 'users',
+ };
+
+ const GROUPS_MOCK_PROPS = {
+ title: 'Groups',
+ projectPath: 'some/project/path',
+ type: 'groups',
+ };
+
+ const groupsAutocompleteQuerySuccess = jest.fn().mockResolvedValue(GROUPS_RESPONSE_MOCK);
+
+ const createComponent = async (props) => {
+ fakeApollo = createMockApollo([[groupsAutocompleteQuery, groupsAutocompleteQuerySuccess]]);
+
+ wrapper = mountExtended(ListSelector, {
+ apolloProvider: fakeApollo,
+ propsData: {
+ ...props,
+ },
+ });
+
+ await waitForPromises();
+ };
+
+ const findCard = () => wrapper.findComponent(GlCard);
+ const findTitle = () => findCard().find('[data-testid="list-selector-title"]');
+ const findIcon = () => wrapper.findComponent(GlIcon);
+ const findAllListBoxComponents = () => wrapper.findAllComponents(GlCollapsibleListbox);
+ const findSearchResultsDropdown = () => findAllListBoxComponents().at(0);
+ const findNamespaceDropdown = () => findAllListBoxComponents().at(1);
+ const findSearchBox = () => wrapper.findComponent(GlSearchBoxByType);
+ const findAllUserComponents = () => wrapper.findAllComponents(UserItem);
+ const findAllGroupComponents = () => wrapper.findAllComponents(GroupItem);
+
+ beforeEach(() => {
+ jest.spyOn(Api, 'projectUsers').mockResolvedValue(USERS_RESPONSE_MOCK);
+ jest.spyOn(Api, 'groupMembers').mockResolvedValue({ data: USERS_RESPONSE_MOCK });
+ });
+
+ describe('Users type', () => {
+ const search = 'foo';
+
+ beforeEach(() => createComponent(USERS_MOCK_PROPS));
+
+ it('renders a Card component', () => {
+ expect(findCard().exists()).toBe(true);
+ });
+
+ it('renders a correct title', () => {
+ expect(findTitle().exists()).toBe(true);
+ expect(findTitle().text()).toContain('Users');
+ });
+
+ it('renders the correct icon', () => {
+ expect(findIcon().props('name')).toBe('user');
+ });
+
+ it('renders a Search box component', () => {
+ expect(findSearchBox().exists()).toBe(true);
+ });
+
+ it('renders two namespace dropdown items', () => {
+ expect(findNamespaceDropdown().props('items').length).toBe(2);
+ });
+
+ it('does not call query when search box has not received an input', () => {
+ expect(Api.projectUsers).not.toHaveBeenCalled();
+ expect(Api.groupMembers).not.toHaveBeenCalled();
+ expect(findAllUserComponents().length).toBe(0);
+ });
+
+ describe.each`
+ dropdownItemValue | apiMethod | apiParams | searchResponse
+ ${'false'} | ${'groupMembers'} | ${[USERS_MOCK_PROPS.groupPath, { query: search }]} | ${USERS_RESPONSE_MOCK}
+ ${'true'} | ${'projectUsers'} | ${[USERS_MOCK_PROPS.projectPath, search]} | ${USERS_RESPONSE_MOCK}
+ `(
+ 'searching based on namespace dropdown selection',
+ ({ dropdownItemValue, apiMethod, apiParams, searchResponse }) => {
+ const emitSearchInput = async () => {
+ findSearchBox().vm.$emit('input', search);
+ await waitForPromises();
+ };
+
+ beforeEach(async () => {
+ findNamespaceDropdown().vm.$emit('select', dropdownItemValue);
+ await emitSearchInput();
+ });
+
+ it('shows error alert when API fails', async () => {
+ jest.spyOn(Api, apiMethod).mockRejectedValueOnce();
+ await emitSearchInput();
+
+ expect(createAlert).toHaveBeenCalledWith({
+ message: 'An error occurred while fetching. Please try again.',
+ });
+ });
+
+ it('calls query with correct variables when Search box receives an input', () => {
+ expect(Api[apiMethod]).toHaveBeenCalledWith(...apiParams);
+ });
+
+ it('renders a List box component with the correct props', () => {
+ expect(findSearchResultsDropdown().props()).toMatchObject({
+ multiple: true,
+ items: searchResponse,
+ });
+ });
+
+ it('renders a user component for each search result', () => {
+ expect(findAllUserComponents().length).toBe(searchResponse.length);
+ });
+
+ it('emits an event when a search result is selected', () => {
+ const firstSearchResult = searchResponse[0];
+ findAllUserComponents().at(0).vm.$emit('select', firstSearchResult.username);
+
+ expect(wrapper.emitted('select')).toEqual([
+ [{ ...firstSearchResult, text: 'Administrator', value: 'root' }],
+ ]);
+ });
+ },
+ );
+
+ describe('selected items', () => {
+ const selectedUser = { username: 'root' };
+ const selectedItems = [selectedUser];
+ beforeEach(() => createComponent({ ...USERS_MOCK_PROPS, selectedItems }));
+
+ it('renders a heading with the total selected items', () => {
+ expect(findTitle().text()).toContain('Users');
+ expect(findTitle().text()).toContain('1');
+ });
+
+ it('renders a user component for each selected item', () => {
+ expect(findAllUserComponents().length).toBe(selectedItems.length);
+ expect(findAllUserComponents().at(0).props()).toMatchObject({
+ data: selectedUser,
+ canDelete: true,
+ });
+ });
+
+ it('emits a delete event when a delete event is emitted from the user component', () => {
+ const username = 'root';
+ findAllUserComponents().at(0).vm.$emit('delete', username);
+
+ expect(wrapper.emitted('delete')).toEqual([[username]]);
+ });
+ });
+ });
+
+ describe('Groups type', () => {
+ beforeEach(() => createComponent(GROUPS_MOCK_PROPS));
+
+ it('renders a correct title', () => {
+ expect(findTitle().exists()).toBe(true);
+ expect(findTitle().text()).toContain('Groups');
+ });
+
+ it('renders the correct icon', () => {
+ expect(findIcon().props('name')).toBe('group');
+ });
+
+ it('does not call query when search box has not received an input', () => {
+ expect(groupsAutocompleteQuerySuccess).not.toHaveBeenCalled();
+ expect(findAllGroupComponents().length).toBe(0);
+ });
+
+ describe('searching', () => {
+ const searchResponse = GROUPS_RESPONSE_MOCK.data.groups.nodes;
+ const search = 'foo';
+
+ const emitSearchInput = async () => {
+ findSearchBox().vm.$emit('input', search);
+ await waitForPromises();
+ };
+
+ beforeEach(() => emitSearchInput());
+
+ it('calls query with correct variables when Search box receives an input', () => {
+ expect(groupsAutocompleteQuerySuccess).toHaveBeenCalledWith({
+ search,
+ });
+ });
+
+ it('renders a dropdown for the search results', () => {
+ expect(findSearchResultsDropdown().props()).toMatchObject({
+ multiple: true,
+ items: searchResponse,
+ });
+ });
+
+ it('renders a group component for each search result', () => {
+ expect(findAllGroupComponents().length).toBe(searchResponse.length);
+ });
+
+ it('emits an event when a search result is selected', () => {
+ const firstSearchResult = searchResponse[0];
+ findAllGroupComponents().at(0).vm.$emit('select', firstSearchResult.name);
+
+ expect(wrapper.emitted('select')).toEqual([
+ [{ ...firstSearchResult, text: 'Flightjs', value: 'Flightjs' }],
+ ]);
+ });
+ });
+
+ describe('selected items', () => {
+ const selectedGroup = { name: 'Flightjs' };
+ const selectedItems = [selectedGroup];
+ beforeEach(() => createComponent({ ...GROUPS_MOCK_PROPS, selectedItems }));
+
+ it('renders a heading with the total selected items', () => {
+ expect(findTitle().text()).toContain('Groups');
+ expect(findTitle().text()).toContain('1');
+ });
+
+ it('renders a group component for each selected item', () => {
+ expect(findAllGroupComponents().length).toBe(selectedItems.length);
+ expect(findAllGroupComponents().at(0).props()).toMatchObject({
+ data: selectedGroup,
+ canDelete: true,
+ });
+ });
+
+ it('emits a delete event when a delete event is emitted from the group component', () => {
+ const name = 'Flightjs';
+ findAllGroupComponents().at(0).vm.$emit('delete', name);
+
+ expect(wrapper.emitted('delete')).toEqual([[name]]);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/list_selector/mock_data.js b/spec/frontend/vue_shared/components/list_selector/mock_data.js
new file mode 100644
index 00000000000..5b44a0c2a83
--- /dev/null
+++ b/spec/frontend/vue_shared/components/list_selector/mock_data.js
@@ -0,0 +1,41 @@
+export const USERS_RESPONSE_MOCK = [
+ {
+ id: 'gid://gitlab/User/1',
+ avatarUrl: '/uploads/-/system/user/avatar/1/avatar.png',
+ name: 'Administrator',
+ username: 'root',
+ __typename: 'AutocompletedUser',
+ },
+ {
+ id: 'gid://gitlab/User/15',
+ avatarUrl:
+ 'https://www.gravatar.com/avatar/c4ab964b90c3049c47882b319d3c5cc0?s=80\u0026d=identicon',
+ name: 'Corrine Rath',
+ username: 'laronda.graham',
+ __typename: 'AutocompletedUser',
+ },
+];
+
+export const GROUPS_RESPONSE_MOCK = {
+ data: {
+ groups: {
+ nodes: [
+ {
+ id: 'gid://gitlab/Group/33',
+ name: 'Flightjs',
+ fullName: 'Flightjs',
+ avatarUrl: null,
+ __typename: 'Group',
+ },
+ {
+ id: 'gid://gitlab/Group/34',
+ name: 'Flight 2',
+ fullName: 'Flight2',
+ avatarUrl: null,
+ __typename: 'Group',
+ },
+ ],
+ __typename: 'GroupConnection',
+ },
+ },
+};
diff --git a/spec/frontend/vue_shared/components/list_selector/user_item_spec.js b/spec/frontend/vue_shared/components/list_selector/user_item_spec.js
new file mode 100644
index 00000000000..d84a29c67e0
--- /dev/null
+++ b/spec/frontend/vue_shared/components/list_selector/user_item_spec.js
@@ -0,0 +1,55 @@
+import { GlAvatar } from '@gitlab/ui';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import UserItem from '~/vue_shared/components/list_selector/user_item.vue';
+
+describe('UserItem spec', () => {
+ let wrapper;
+
+ const MOCK_USER = { name: 'Admin', username: 'root', avatarUrl: 'some/avatar.jpg' };
+
+ const createComponent = (props) => {
+ wrapper = mountExtended(UserItem, {
+ propsData: {
+ data: MOCK_USER,
+ ...props,
+ },
+ });
+ };
+
+ const findAvatar = () => wrapper.findComponent(GlAvatar);
+ const findDeleteButton = () => wrapper.findByRole('button', { name: 'Delete Admin' });
+
+ beforeEach(() => createComponent());
+
+ it('renders an Avatar component', () => {
+ expect(findAvatar().props('size')).toBe(32);
+ expect(findAvatar().attributes()).toMatchObject({
+ src: MOCK_USER.avatarUrl,
+ alt: MOCK_USER.name,
+ });
+ });
+
+ it('renders a name and username', () => {
+ expect(wrapper.text()).toContain('Admin');
+ expect(wrapper.text()).toContain('@root');
+ });
+
+ it('does not render a delete button by default', () => {
+ expect(findDeleteButton().exists()).toBe(false);
+ });
+
+ describe('Delete button', () => {
+ beforeEach(() => createComponent({ canDelete: true }));
+
+ it('renders a delete button', () => {
+ expect(findDeleteButton().exists()).toBe(true);
+ expect(findDeleteButton().props('icon')).toBe('remove');
+ });
+
+ it('emits a delete event if the delete button is clicked', () => {
+ findDeleteButton().trigger('click');
+
+ expect(wrapper.emitted('delete')).toEqual([[MOCK_USER.username]]);
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/markdown/markdown_editor_spec.js b/spec/frontend/vue_shared/components/markdown/markdown_editor_spec.js
index b4c90fe49d1..edb11bd581b 100644
--- a/spec/frontend/vue_shared/components/markdown/markdown_editor_spec.js
+++ b/spec/frontend/vue_shared/components/markdown/markdown_editor_spec.js
@@ -137,7 +137,26 @@ describe('vue_shared/component/markdown/markdown_editor', () => {
await enableContentEditor();
expect(mock.history.post).toHaveLength(1);
- expect(mock.history.post[0].url).toContain(`render_quick_actions=true`);
+ expect(mock.history.post[0].url).toBe(
+ `${window.location.origin}/api/markdown?render_quick_actions=true`,
+ );
+ });
+
+ describe('if gitlab is installed under a relative url', () => {
+ beforeEach(() => {
+ window.gon = { relative_url_root: '/gitlab' };
+ });
+
+ it('passes render_quick_actions param to renderMarkdownPath if quick actions are enabled', async () => {
+ buildWrapper({ propsData: { supportsQuickActions: true } });
+
+ await enableContentEditor();
+
+ expect(mock.history.post).toHaveLength(1);
+ expect(mock.history.post[0].url).toBe(
+ `${window.location.origin}/gitlab/api/markdown?render_quick_actions=true`,
+ );
+ });
});
it('does not pass render_quick_actions param to renderMarkdownPath if quick actions are disabled', async () => {
@@ -146,7 +165,9 @@ describe('vue_shared/component/markdown/markdown_editor', () => {
await enableContentEditor();
expect(mock.history.post).toHaveLength(1);
- expect(mock.history.post[0].url).toContain(`render_quick_actions=false`);
+ expect(mock.history.post[0].url).toBe(
+ `${window.location.origin}/api/markdown?render_quick_actions=false`,
+ );
});
it('enables content editor switcher when contentEditorEnabled prop is true', () => {
diff --git a/spec/frontend/vue_shared/components/notes/__snapshots__/noteable_warning_spec.js.snap b/spec/frontend/vue_shared/components/notes/__snapshots__/noteable_warning_spec.js.snap
index 891b0c95f0e..ad0e260ad70 100644
--- a/spec/frontend/vue_shared/components/notes/__snapshots__/noteable_warning_spec.js.snap
+++ b/spec/frontend/vue_shared/components/notes/__snapshots__/noteable_warning_spec.js.snap
@@ -2,7 +2,7 @@
exports[`Issue Warning Component when issue is locked but not confidential renders information about locked issue 1`] = `
<span>
- This issue is locked. Only project members can comment.
+ The discussion in this issue is locked. Only project members can comment.
<gl-link-stub
href="locked-path"
target="_blank"
@@ -34,12 +34,12 @@ exports[`Issue Warning Component when noteable is locked and confidential render
>
confidential
</gl-link-stub>
- and
+ and its
<gl-link-stub
href=""
target="_blank"
>
- locked
+ discussion is locked
</gl-link-stub>
.
</span>
diff --git a/spec/frontend/vue_shared/components/notes/noteable_warning_spec.js b/spec/frontend/vue_shared/components/notes/noteable_warning_spec.js
index d7fcb9a25d4..d73356e00da 100644
--- a/spec/frontend/vue_shared/components/notes/noteable_warning_spec.js
+++ b/spec/frontend/vue_shared/components/notes/noteable_warning_spec.js
@@ -126,12 +126,14 @@ describe('Issue Warning Component', () => {
});
it('renders confidential & locked messages with noteable "issue"', () => {
- expect(findLockedBlock(wrapperLocked).text()).toContain('This issue is locked.');
+ expect(findLockedBlock(wrapperLocked).text()).toContain(
+ 'The discussion in this issue is locked.',
+ );
expect(findConfidentialBlock(wrapperConfidential).text()).toContain(
'This is a confidential issue.',
);
expect(findLockedAndConfidentialBlock(wrapperLockedAndConfidential).text()).toContain(
- 'This issue is confidential and locked.',
+ 'This issue is confidential and its discussion is locked.',
);
});
@@ -147,7 +149,9 @@ describe('Issue Warning Component', () => {
});
await nextTick();
- expect(findLockedBlock(wrapperLocked).text()).toContain('This epic is locked.');
+ expect(findLockedBlock(wrapperLocked).text()).toContain(
+ 'The discussion in this epic is locked.',
+ );
await nextTick();
expect(findConfidentialBlock(wrapperConfidential).text()).toContain(
@@ -156,7 +160,7 @@ describe('Issue Warning Component', () => {
await nextTick();
expect(findLockedAndConfidentialBlock(wrapperLockedAndConfidential).text()).toContain(
- 'This epic is confidential and locked.',
+ 'This epic is confidential and its discussion is locked.',
);
});
@@ -172,7 +176,9 @@ describe('Issue Warning Component', () => {
});
await nextTick();
- expect(findLockedBlock(wrapperLocked).text()).toContain('This merge request is locked.');
+ expect(findLockedBlock(wrapperLocked).text()).toContain(
+ 'The discussion in this merge request is locked.',
+ );
await nextTick();
expect(findConfidentialBlock(wrapperConfidential).text()).toContain(
@@ -181,7 +187,7 @@ describe('Issue Warning Component', () => {
await nextTick();
expect(findLockedAndConfidentialBlock(wrapperLockedAndConfidential).text()).toContain(
- 'This merge request is confidential and locked.',
+ 'This merge request is confidential and its discussion is locked.',
);
});
});
diff --git a/spec/frontend/vue_shared/components/notes/system_note_spec.js b/spec/frontend/vue_shared/components/notes/system_note_spec.js
index 7f3912dcadb..ade97290004 100644
--- a/spec/frontend/vue_shared/components/notes/system_note_spec.js
+++ b/spec/frontend/vue_shared/components/notes/system_note_spec.js
@@ -61,10 +61,16 @@ describe('system note component', () => {
expect(vm.classes()).toContain('target');
});
- it('should render svg icon', () => {
+ it('should render svg icon only for certain icons', () => {
+ const ALLOWED_ICONS = ['check', 'merge', 'merge-request-close', 'issue-close'];
createComponent(props);
- expect(vm.find('[data-testid="timeline-icon"]').exists()).toBe(true);
+ expect(vm.find('[data-testid="timeline-icon"]').exists()).toBe(false);
+
+ ALLOWED_ICONS.forEach((icon) => {
+ createComponent({ note: { ...props.note, system_note_icon_name: icon } });
+ expect(vm.find('[data-testid="timeline-icon"]').exists()).toBe(true);
+ });
});
// Redcarpet Markdown renderer wraps text in `<p>` tags
diff --git a/spec/frontend/vue_shared/components/source_viewer/components/blame_info_spec.js b/spec/frontend/vue_shared/components/source_viewer/components/blame_info_spec.js
index ff8b2be9634..121bc691041 100644
--- a/spec/frontend/vue_shared/components/source_viewer/components/blame_info_spec.js
+++ b/spec/frontend/vue_shared/components/source_viewer/components/blame_info_spec.js
@@ -1,42 +1,35 @@
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import { setHTMLFixture } from 'helpers/fixtures';
import CommitInfo from '~/repository/components/commit_info.vue';
import BlameInfo from '~/vue_shared/components/source_viewer/components/blame_info.vue';
-import * as utils from '~/vue_shared/components/source_viewer/utils';
-import { SOURCE_CODE_CONTENT_MOCK, BLAME_DATA_MOCK } from '../mock_data';
+import { BLAME_DATA_MOCK } from '../mock_data';
describe('BlameInfo component', () => {
let wrapper;
const createComponent = () => {
wrapper = shallowMountExtended(BlameInfo, {
- propsData: { blameData: BLAME_DATA_MOCK },
+ propsData: { blameInfo: BLAME_DATA_MOCK },
});
};
beforeEach(() => {
- setHTMLFixture(SOURCE_CODE_CONTENT_MOCK);
- jest.spyOn(utils, 'toggleBlameClasses');
createComponent();
});
const findCommitInfoComponents = () => wrapper.findAllComponents(CommitInfo);
- it('adds the necessary classes to the DOM', () => {
- expect(utils.toggleBlameClasses).toHaveBeenCalledWith(BLAME_DATA_MOCK, true);
- });
-
it('renders a CommitInfo component for each blame entry', () => {
expect(findCommitInfoComponents().length).toBe(BLAME_DATA_MOCK.length);
});
it.each(BLAME_DATA_MOCK)(
'sets the correct data and positioning for the commitInfo',
- ({ lineno, commit, index }) => {
+ ({ commit, commitData, index, blameOffset }) => {
const commitInfoComponent = findCommitInfoComponents().at(index);
expect(commitInfoComponent.props('commit')).toEqual(commit);
- expect(commitInfoComponent.element.style.top).toBe(utils.calculateBlameOffset(lineno));
+ expect(commitInfoComponent.props('prevBlameLink')).toBe(commitData?.projectBlameLink || null);
+ expect(commitInfoComponent.element.style.top).toBe(blameOffset);
},
);
@@ -52,12 +45,4 @@ describe('BlameInfo component', () => {
expect(findCommitInfoComponents().at(2).element.classList).toContain(borderTopClassName);
});
});
-
- describe('when component is destroyed', () => {
- beforeEach(() => wrapper.destroy());
-
- it('resets the DOM to its original state', () => {
- expect(utils.toggleBlameClasses).toHaveBeenCalledWith(BLAME_DATA_MOCK, false);
- });
- });
});
diff --git a/spec/frontend/vue_shared/components/source_viewer/components/chunk_new_spec.js b/spec/frontend/vue_shared/components/source_viewer/components/chunk_new_spec.js
index 852598b13dc..c7b2363026a 100644
--- a/spec/frontend/vue_shared/components/source_viewer/components/chunk_new_spec.js
+++ b/spec/frontend/vue_shared/components/source_viewer/components/chunk_new_spec.js
@@ -6,7 +6,6 @@ import { CHUNK_1, CHUNK_2 } from '../mock_data';
describe('Chunk component', () => {
let wrapper;
- let idleCallbackSpy;
const createComponent = (props = {}) => {
wrapper = shallowMountExtended(Chunk, {
@@ -19,7 +18,6 @@ describe('Chunk component', () => {
const findContent = () => wrapper.findByTestId('content');
beforeEach(() => {
- idleCallbackSpy = jest.spyOn(window, 'requestIdleCallback').mockImplementation((fn) => fn());
createComponent();
});
@@ -40,19 +38,6 @@ describe('Chunk component', () => {
});
describe('rendering', () => {
- it('does not register window.requestIdleCallback for the first chunk, renders content immediately', () => {
- expect(window.requestIdleCallback).not.toHaveBeenCalled();
- expect(findContent().text()).toBe(CHUNK_1.highlightedContent);
- });
-
- it('does not render content if browser is not in idle state', () => {
- idleCallbackSpy.mockRestore();
- createComponent({ chunkIndex: 1, ...CHUNK_2 });
-
- expect(findLineNumbers()).toHaveLength(0);
- expect(findContent().exists()).toBe(false);
- });
-
describe('isHighlighted is false', () => {
beforeEach(() => createComponent(CHUNK_2));
diff --git a/spec/frontend/vue_shared/components/source_viewer/mock_data.js b/spec/frontend/vue_shared/components/source_viewer/mock_data.js
index b3516f7ed72..cfff3a15b77 100644
--- a/spec/frontend/vue_shared/components/source_viewer/mock_data.js
+++ b/spec/frontend/vue_shared/components/source_viewer/mock_data.js
@@ -24,22 +24,74 @@ export const CHUNK_2 = {
};
export const SOURCE_CODE_CONTENT_MOCK = `
-<div class="content">
- <div>
- <div id="L1">1</div>
- <div id="L2">2</div>
- <div id="L3">3</div>
- </div>
+<div class="file-holder">
+ <div class="blob-viewer">
+ <div class="content">
+ <div>
+ <div id="L1">1</div>
+ <div id="L2">2</div>
+ <div id="L3">3</div>
+ </div>
- <div>
- <div id="LC1">Content 1</div>
- <div id="LC2">Content 2</div>
- <div id="LC3">Content 3</div>
+ <div>
+ <div id="LC1">Content 1</div>
+ <div id="LC2">Content 2</div>
+ <div id="LC3">Content 3</div>
+ </div>
+ </div>
</div>
</div>`;
+const COMMIT_DATA_MOCK = { projectBlameLink: 'project/blame/link' };
+
export const BLAME_DATA_MOCK = [
- { lineno: 1, commit: { author: 'Peter' }, index: 0 },
- { lineno: 2, commit: { author: 'Sarah' }, index: 1 },
- { lineno: 3, commit: { author: 'Peter' }, index: 2 },
+ {
+ lineno: 1,
+ commit: { author: 'Peter', sha: 'abc' },
+ index: 0,
+ blameOffset: '0px',
+ commitData: COMMIT_DATA_MOCK,
+ },
+ { lineno: 2, commit: { author: 'Sarah', sha: 'def' }, index: 1, blameOffset: '1px' },
+ { lineno: 3, commit: { author: 'Peter', sha: 'ghi' }, index: 2, blameOffset: '2px' },
];
+
+export const BLAME_DATA_QUERY_RESPONSE_MOCK = {
+ data: {
+ project: {
+ id: 'gid://gitlab/Project/278964',
+ __typename: 'Project',
+ repository: {
+ __typename: 'Repository',
+ blobs: {
+ __typename: 'BlobConnection',
+ nodes: [
+ {
+ id: 'gid://gitlab/Blob/f0c77e4b621df72719ce2b500ea6228559f6bc09',
+ blame: {
+ firstLine: '1',
+ groups: [
+ {
+ lineno: 1,
+ span: 3,
+ commit: {
+ id: 'gid://gitlab/CommitPresenter/13b0aca4142d1d55931577f69289a792f216f805',
+ titleHtml: 'Upload New File',
+ message: 'Upload New File',
+ authoredDate: '2022-10-31T10:38:30+00:00',
+ authorGravatar: 'path/to/gravatar',
+ webPath: '/commit/1234',
+ author: {},
+ sha: '13b0aca4142d1d55931577f69289a792f216f805',
+ },
+ commitData: COMMIT_DATA_MOCK,
+ },
+ ],
+ },
+ },
+ ],
+ },
+ },
+ },
+ },
+};
diff --git a/spec/frontend/vue_shared/components/source_viewer/source_viewer_new_spec.js b/spec/frontend/vue_shared/components/source_viewer/source_viewer_new_spec.js
index 1a498d0c5b1..ee7164515f6 100644
--- a/spec/frontend/vue_shared/components/source_viewer/source_viewer_new_spec.js
+++ b/spec/frontend/vue_shared/components/source_viewer/source_viewer_new_spec.js
@@ -1,11 +1,29 @@
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import createMockApollo from 'helpers/mock_apollo_helper';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import { setHTMLFixture } from 'helpers/fixtures';
import SourceViewer from '~/vue_shared/components/source_viewer/source_viewer_new.vue';
import Chunk from '~/vue_shared/components/source_viewer/components/chunk_new.vue';
import { EVENT_ACTION, EVENT_LABEL_VIEWER } from '~/vue_shared/components/source_viewer/constants';
import Tracking from '~/tracking';
import LineHighlighter from '~/blob/line_highlighter';
import addBlobLinksTracking from '~/blob/blob_links_tracking';
-import { BLOB_DATA_MOCK, CHUNK_1, CHUNK_2, LANGUAGE_MOCK } from './mock_data';
+import waitForPromises from 'helpers/wait_for_promises';
+import blameDataQuery from '~/vue_shared/components/source_viewer/queries/blame_data.query.graphql';
+import Blame from '~/vue_shared/components/source_viewer/components/blame_info.vue';
+import * as utils from '~/vue_shared/components/source_viewer/utils';
+
+import {
+ BLOB_DATA_MOCK,
+ CHUNK_1,
+ CHUNK_2,
+ LANGUAGE_MOCK,
+ BLAME_DATA_QUERY_RESPONSE_MOCK,
+ SOURCE_CODE_CONTENT_MOCK,
+} from './mock_data';
+
+Vue.use(VueApollo);
const lineHighlighter = new LineHighlighter();
jest.mock('~/blob/line_highlighter', () =>
@@ -17,17 +35,35 @@ jest.mock('~/blob/blob_links_tracking');
describe('Source Viewer component', () => {
let wrapper;
+ let fakeApollo;
const CHUNKS_MOCK = [CHUNK_1, CHUNK_2];
const hash = '#L142';
- const createComponent = () => {
+ const blameDataQueryHandlerSuccess = jest.fn().mockResolvedValue(BLAME_DATA_QUERY_RESPONSE_MOCK);
+ const blameInfo =
+ BLAME_DATA_QUERY_RESPONSE_MOCK.data.project.repository.blobs.nodes[0].blame.groups;
+
+ const createComponent = ({ showBlame = true } = {}) => {
+ fakeApollo = createMockApollo([[blameDataQuery, blameDataQueryHandlerSuccess]]);
+
wrapper = shallowMountExtended(SourceViewer, {
+ apolloProvider: fakeApollo,
mocks: { $route: { hash } },
- propsData: { blob: BLOB_DATA_MOCK, chunks: CHUNKS_MOCK },
+ propsData: {
+ blob: BLOB_DATA_MOCK,
+ chunks: CHUNKS_MOCK,
+ projectPath: 'test',
+ showBlame,
+ },
});
};
const findChunks = () => wrapper.findAllComponents(Chunk);
+ const findBlameComponents = () => wrapper.findAllComponents(Blame);
+ const triggerChunkAppear = async (chunkIndex = 0) => {
+ findChunks().at(chunkIndex).vm.$emit('appear');
+ await waitForPromises();
+ };
beforeEach(() => {
jest.spyOn(Tracking, 'event');
@@ -50,6 +86,65 @@ describe('Source Viewer component', () => {
});
describe('rendering', () => {
+ it('does not render a Blame component if the respective chunk for the blame has not appeared', async () => {
+ await waitForPromises();
+ expect(findBlameComponents()).toHaveLength(0);
+ });
+
+ describe('DOM updates', () => {
+ it('adds the necessary classes to the DOM', async () => {
+ setHTMLFixture(SOURCE_CODE_CONTENT_MOCK);
+ jest.spyOn(utils, 'toggleBlameClasses');
+ createComponent();
+ await triggerChunkAppear();
+
+ expect(utils.toggleBlameClasses).toHaveBeenCalledWith(blameInfo, true);
+ });
+ });
+
+ describe('Blame information', () => {
+ it('renders a Blame component when a chunk appears', async () => {
+ await triggerChunkAppear();
+
+ expect(findBlameComponents().at(0).exists()).toBe(true);
+ expect(findBlameComponents().at(0).props()).toMatchObject({ blameInfo });
+ });
+
+ it('calls the query only once per chunk', async () => {
+ jest.spyOn(wrapper.vm.$apollo, 'query');
+
+ // We trigger the `appear` event multiple times here in order to simulate the user scrolling past the chunk more than once.
+ // In this scenario we only want to query the backend once.
+ await triggerChunkAppear();
+ await triggerChunkAppear();
+
+ expect(wrapper.vm.$apollo.query).toHaveBeenCalledTimes(1);
+ });
+
+ it('requests blame information for overlapping chunk', async () => {
+ jest.spyOn(wrapper.vm.$apollo, 'query');
+
+ await triggerChunkAppear(1);
+
+ expect(wrapper.vm.$apollo.query).toHaveBeenCalledTimes(2);
+ expect(blameDataQueryHandlerSuccess).toHaveBeenCalledWith(
+ expect.objectContaining({ fromLine: 71, toLine: 110 }),
+ );
+ expect(blameDataQueryHandlerSuccess).toHaveBeenCalledWith(
+ expect.objectContaining({ fromLine: 1, toLine: 70 }),
+ );
+
+ expect(findChunks().at(0).props('isHighlighted')).toBe(true);
+ });
+
+ it('does not render a Blame component when `showBlame: false`', async () => {
+ createComponent({ showBlame: false });
+ await triggerChunkAppear();
+
+ expect(findBlameComponents()).toHaveLength(0);
+ });
+ });
+
it('renders a Chunk component for each chunk', () => {
expect(findChunks().at(0).props()).toMatchObject(CHUNK_1);
expect(findChunks().at(1).props()).toMatchObject(CHUNK_2);
@@ -58,8 +153,7 @@ describe('Source Viewer component', () => {
describe('hash highlighting', () => {
it('calls highlightHash with expected parameter', () => {
- const scrollEnabled = false;
- expect(lineHighlighter.highlightHash).toHaveBeenCalledWith(hash, scrollEnabled);
+ expect(lineHighlighter.highlightHash).toHaveBeenCalledWith(hash);
});
});
});
diff --git a/spec/frontend/vue_shared/components/source_viewer/utils_spec.js b/spec/frontend/vue_shared/components/source_viewer/utils_spec.js
index 0ac72aa9afb..a656b06068b 100644
--- a/spec/frontend/vue_shared/components/source_viewer/utils_spec.js
+++ b/spec/frontend/vue_shared/components/source_viewer/utils_spec.js
@@ -1,6 +1,7 @@
import { setHTMLFixture } from 'helpers/fixtures';
import {
calculateBlameOffset,
+ shouldRender,
toggleBlameClasses,
} from '~/vue_shared/components/source_viewer/utils';
import { SOURCE_CODE_CONTENT_MOCK, BLAME_DATA_MOCK } from './mock_data';
@@ -21,6 +22,19 @@ describe('SourceViewer utils', () => {
});
});
+ describe('shouldRender', () => {
+ const commit = { sha: 'abc' };
+ const identicalSha = [{ commit }, { commit }];
+
+ it.each`
+ data | index | result
+ ${identicalSha} | ${0} | ${true}
+ ${identicalSha} | ${1} | ${false}
+ `('returns $result', ({ data, index, result }) => {
+ expect(shouldRender(data, index)).toBe(result);
+ });
+ });
+
describe('toggleBlameClasses', () => {
it('adds classes', () => {
toggleBlameClasses(BLAME_DATA_MOCK, true);
diff --git a/spec/frontend/vue_shared/components/users_table/mock_data.js b/spec/frontend/vue_shared/components/users_table/mock_data.js
new file mode 100644
index 00000000000..c763ca2ca9b
--- /dev/null
+++ b/spec/frontend/vue_shared/components/users_table/mock_data.js
@@ -0,0 +1,23 @@
+export const MOCK_USERS = [
+ {
+ id: 2177,
+ name: 'Nikki',
+ createdAt: '2020-11-13T12:26:54.177Z',
+ email: 'nikki@example.com',
+ username: 'nikki',
+ lastActivityOn: '2020-12-09',
+ avatarUrl:
+ 'https://secure.gravatar.com/avatar/054f062d8b1a42b123f17e13a173cda8?s=80\\u0026d=identicon',
+ badges: [
+ { text: 'Admin', variant: 'success' },
+ { text: "It's you!", variant: 'muted' },
+ ],
+ projectsCount: 0,
+ actions: [],
+ note: 'Create per issue #999',
+ },
+];
+
+export const MOCK_ADMIN_USER_PATH = 'admin/users/:id';
+
+export const MOCK_GROUP_COUNTS = { 2177: 5 };
diff --git a/spec/frontend/admin/users/components/user_avatar_spec.js b/spec/frontend/vue_shared/components/users_table/user_avatar_spec.js
index 02e648d2b77..035778530af 100644
--- a/spec/frontend/admin/users/components/user_avatar_spec.js
+++ b/spec/frontend/vue_shared/components/users_table/user_avatar_spec.js
@@ -2,15 +2,14 @@ import { GlAvatarLabeled, GlBadge, GlIcon } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
-import AdminUserAvatar from '~/admin/users/components/user_avatar.vue';
-import { LENGTH_OF_USER_NOTE_TOOLTIP } from '~/admin/users/constants';
+import AdminUserAvatar from '~/vue_shared/components/users_table/user_avatar.vue';
+import { LENGTH_OF_USER_NOTE_TOOLTIP } from '~/vue_shared/components/users_table/constants';
import { truncate } from '~/lib/utils/text_utility';
-import { users, paths } from '../mock_data';
+import { MOCK_USERS, MOCK_ADMIN_USER_PATH } from './mock_data';
describe('AdminUserAvatar component', () => {
let wrapper;
- const user = users[0];
- const adminUserPath = paths.adminUser;
+ const user = MOCK_USERS[0];
const findNote = () => wrapper.findComponent(GlIcon);
const findAvatar = () => wrapper.findComponent(GlAvatarLabeled);
@@ -22,7 +21,7 @@ describe('AdminUserAvatar component', () => {
wrapper = shallowMount(AdminUserAvatar, {
propsData: {
user,
- adminUserPath,
+ adminUserPath: MOCK_ADMIN_USER_PATH,
...props,
},
directives: {
@@ -50,14 +49,7 @@ describe('AdminUserAvatar component', () => {
const avatar = findAvatar();
expect(avatar.props('label')).toBe(user.name);
- expect(avatar.props('labelLink')).toBe(adminUserPath.replace('id', user.username));
- });
-
- it("renders the user's email with a mailto link", () => {
- const avatar = findAvatar();
-
- expect(avatar.props('subLabel')).toBe(user.email);
- expect(avatar.props('subLabelLink')).toBe(`mailto:${user.email}`);
+ expect(avatar.props('labelLink')).toBe(MOCK_ADMIN_USER_PATH.replace('id', user.username));
});
it("renders the user's avatar image", () => {
@@ -118,4 +110,30 @@ describe('AdminUserAvatar component', () => {
});
});
});
+
+ describe('when user has an email address', () => {
+ beforeEach(() => {
+ initComponent();
+ });
+
+ it("renders the user's email with a mailto link", () => {
+ const avatar = findAvatar();
+
+ expect(avatar.props('subLabel')).toBe(user.email);
+ expect(avatar.props('subLabelLink')).toBe(`mailto:${user.email}`);
+ });
+ });
+
+ describe('when user does not have an email address', () => {
+ beforeEach(() => {
+ initComponent({ user: { ...MOCK_USERS[0], email: null } });
+ });
+
+ it("renders the user's username without a link", () => {
+ const avatar = findAvatar();
+
+ expect(avatar.props('subLabel')).toBe(`@${user.username}`);
+ expect(avatar.props('subLabelLink')).toBe('');
+ });
+ });
});
diff --git a/spec/frontend/vue_shared/components/users_table/users_table_spec.js b/spec/frontend/vue_shared/components/users_table/users_table_spec.js
new file mode 100644
index 00000000000..45d1d291d47
--- /dev/null
+++ b/spec/frontend/vue_shared/components/users_table/users_table_spec.js
@@ -0,0 +1,95 @@
+import { GlTable, GlSkeletonLoader } from '@gitlab/ui';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import UsersTable from '~/vue_shared/components/users_table/users_table.vue';
+import UserAvatar from '~/vue_shared/components/users_table/user_avatar.vue';
+import UserDate from '~/vue_shared/components/user_date.vue';
+import { MOCK_USERS, MOCK_ADMIN_USER_PATH, MOCK_GROUP_COUNTS } from './mock_data';
+
+describe('UsersTable component', () => {
+ let wrapper;
+ const user = MOCK_USERS[0];
+
+ const findUserGroupCount = (id) => wrapper.findByTestId(`user-group-count-${id}`);
+ const findUserGroupCountLoader = (id) => findUserGroupCount(id).findComponent(GlSkeletonLoader);
+ const getCellByLabel = (trIdx, label) => {
+ return wrapper
+ .findComponent(GlTable)
+ .find('tbody')
+ .findAll('tr')
+ .at(trIdx)
+ .find(`[data-label="${label}"][role="cell"]`);
+ };
+
+ const initComponent = (props = {}) => {
+ wrapper = mountExtended(UsersTable, {
+ propsData: {
+ users: MOCK_USERS,
+ adminUserPath: MOCK_ADMIN_USER_PATH,
+ groupCounts: MOCK_GROUP_COUNTS,
+ groupCountsLoading: false,
+ ...props,
+ },
+ });
+ };
+
+ describe('when there are users', () => {
+ beforeEach(() => {
+ initComponent();
+ });
+
+ it('renders the projects count', () => {
+ expect(getCellByLabel(0, 'Projects').text()).toContain(`${user.projectsCount}`);
+ });
+
+ it.each`
+ component | label
+ ${UserAvatar} | ${'Name'}
+ ${UserDate} | ${'Created on'}
+ ${UserDate} | ${'Last activity'}
+ `('renders the component for column $label', ({ component, label }) => {
+ expect(getCellByLabel(0, label).findComponent(component).exists()).toBe(true);
+ });
+ });
+
+ describe('when users is an empty array', () => {
+ beforeEach(() => {
+ initComponent({ users: [] });
+ });
+
+ it('renders a "No users found" message', () => {
+ expect(wrapper.text()).toContain('No users found');
+ });
+ });
+
+ describe('group counts', () => {
+ describe('when groupCountsLoading is true', () => {
+ beforeEach(() => {
+ initComponent({ groupCountsLoading: true });
+ });
+
+ it('renders a loader for each user', () => {
+ expect(findUserGroupCountLoader(user.id).exists()).toBe(true);
+ });
+ });
+
+ describe('when groupCounts has data', () => {
+ beforeEach(() => {
+ initComponent();
+ });
+
+ it("renders the user's group count", () => {
+ expect(findUserGroupCount(user.id).text()).toBe('5');
+ });
+ });
+
+ describe('when groupCounts has no data', () => {
+ beforeEach(() => {
+ initComponent({ groupCounts: {} });
+ });
+
+ it("renders the user's group count as 0", () => {
+ expect(findUserGroupCount(user.id).text()).toBe('0');
+ });
+ });
+ });
+});
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 56d89d428f7..84ecbb431ac 100644
--- a/spec/frontend/vue_shared/components/web_ide_link_spec.js
+++ b/spec/frontend/vue_shared/components/web_ide_link_spec.js
@@ -35,7 +35,7 @@ const ACTION_EDIT = {
text: 'Edit single file',
secondaryText: 'Edit this file only.',
attrs: {
- 'data-qa-selector': 'edit_menu_item',
+ 'data-testid': 'edit-menu-item',
},
tracking: {
action: 'click_consolidated_edit',
@@ -51,7 +51,7 @@ const ACTION_WEB_IDE = {
secondaryText: i18n.webIdeText,
text: 'Web IDE',
attrs: {
- 'data-qa-selector': 'webide_menu_item',
+ 'data-testid': 'webide-menu-item',
},
href: undefined,
handle: expect.any(Function),
@@ -71,7 +71,7 @@ const ACTION_GITPOD = {
secondaryText: 'Launch a ready-to-code development environment for your project.',
text: 'Gitpod',
attrs: {
- 'data-qa-selector': 'gitpod_menu_item',
+ 'data-testid': 'gitpod-menu-item',
},
tracking: {
action: 'click_consolidated_edit',
@@ -88,7 +88,7 @@ const ACTION_PIPELINE_EDITOR = {
secondaryText: 'Edit, lint, and visualize your pipeline.',
text: 'Edit in pipeline editor',
attrs: {
- 'data-qa-selector': 'pipeline_editor_menu_item',
+ 'data-testid': 'pipeline_editor-menu-item',
},
tracking: {
action: 'click_consolidated_edit',
@@ -149,7 +149,7 @@ describe('vue_shared/components/web_ide_link', () => {
href: props.item.href,
handle: props.item.handle,
attrs: {
- 'data-qa-selector': attributes['data-qa-selector'],
+ 'data-testid': attributes['data-testid'],
},
};
});
diff --git a/spec/frontend/vue_shared/issuable/show/components/issuable_body_spec.js b/spec/frontend/vue_shared/issuable/show/components/issuable_body_spec.js
index 02e729a00bd..71ff5275063 100644
--- a/spec/frontend/vue_shared/issuable/show/components/issuable_body_spec.js
+++ b/spec/frontend/vue_shared/issuable/show/components/issuable_body_spec.js
@@ -133,6 +133,7 @@ describe('IssuableBody', () => {
issuable: issuableBodyProps.issuable,
statusIcon: issuableBodyProps.statusIcon,
enableEdit: issuableBodyProps.enableEdit,
+ workspaceType: issuableBodyProps.workspaceType,
});
});
diff --git a/spec/frontend/vue_shared/issuable/show/components/issuable_show_root_spec.js b/spec/frontend/vue_shared/issuable/show/components/issuable_show_root_spec.js
index ad7afefff12..6d1d3773643 100644
--- a/spec/frontend/vue_shared/issuable/show/components/issuable_show_root_spec.js
+++ b/spec/frontend/vue_shared/issuable/show/components/issuable_show_root_spec.js
@@ -52,6 +52,7 @@ describe('IssuableShowRoot', () => {
descriptionPreviewPath,
descriptionHelpPath,
taskCompletionStatus,
+ workspaceType,
} = mockIssuableShowProps;
const { state, blocked, confidential, createdAt, author } = mockIssuable;
@@ -92,6 +93,7 @@ describe('IssuableShowRoot', () => {
editFormVisible,
descriptionPreviewPath,
descriptionHelpPath,
+ workspaceType,
});
});
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 eefc9142064..0ea69bc27e5 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
@@ -4,6 +4,7 @@ import { nextTick } from 'vue';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import IssuableTitle from '~/vue_shared/issuable/show/components/issuable_title.vue';
+import ConfidentialityBadge from '~/vue_shared/components/confidentiality_badge.vue';
import { mockIssuableShowProps, mockIssuable } from '../mock_data';
@@ -86,19 +87,39 @@ describe('IssuableTitle', () => {
expect(tooltip).toBeDefined();
});
- it('renders sticky header when `stickyTitleVisible` prop is true', async () => {
- wrapper.findComponent(GlIntersectionObserver).vm.$emit('disappear');
- await nextTick();
+ describe('sticky header', () => {
+ it('renders when `stickyTitleVisible` prop is true', async () => {
+ wrapper.findComponent(GlIntersectionObserver).vm.$emit('disappear');
+ await nextTick();
- const stickyHeaderEl = findStickyHeader();
+ const stickyHeaderEl = findStickyHeader();
- expect(stickyHeaderEl.exists()).toBe(true);
- expect(stickyHeaderEl.findComponent(GlBadge).props('variant')).toBe('success');
- expect(stickyHeaderEl.findComponent(GlIcon).props('name')).toBe(
- issuableTitleProps.statusIcon,
- );
- expect(stickyHeaderEl.text()).toContain('Open');
- expect(stickyHeaderEl.text()).toContain(issuableTitleProps.issuable.title);
+ expect(stickyHeaderEl.exists()).toBe(true);
+ expect(stickyHeaderEl.findComponent(GlBadge).props('variant')).toBe('success');
+ expect(stickyHeaderEl.findComponent(GlIcon).props('name')).toBe(
+ issuableTitleProps.statusIcon,
+ );
+ expect(stickyHeaderEl.text()).toContain('Open');
+ expect(stickyHeaderEl.findComponent(ConfidentialityBadge).exists()).toBe(false);
+ expect(stickyHeaderEl.text()).toContain(issuableTitleProps.issuable.title);
+ });
+
+ it('renders ConfidentialityBadge when issuable is confidential', async () => {
+ wrapper = createComponent({
+ ...mockIssuableShowProps,
+ issuable: {
+ ...mockIssuable,
+ confidential: true,
+ },
+ });
+
+ wrapper.findComponent(GlIntersectionObserver).vm.$emit('disappear');
+ await nextTick();
+
+ const stickyHeaderEl = findStickyHeader();
+
+ expect(stickyHeaderEl.findComponent(ConfidentialityBadge).exists()).toBe(true);
+ });
});
});
});
diff --git a/spec/frontend/vue_shared/mixins/gl_feature_flags_mixin_spec.js b/spec/frontend/vue_shared/mixins/gl_feature_flags_mixin_spec.js
index 3ce12caf95a..1f579a1e945 100644
--- a/spec/frontend/vue_shared/mixins/gl_feature_flags_mixin_spec.js
+++ b/spec/frontend/vue_shared/mixins/gl_feature_flags_mixin_spec.js
@@ -19,7 +19,7 @@ describe('GitLab Feature Flags Mixin', () => {
wrapper = shallowMount(component, {
provide: {
- glFeatures: { ...(gon.features || {}) },
+ glFeatures: { ...gon.features },
},
});
});
diff --git a/spec/frontend/webhooks/components/__snapshots__/push_events_spec.js.snap b/spec/frontend/webhooks/components/__snapshots__/push_events_spec.js.snap
index 4150bd75c16..56f8bf0a5e3 100644
--- a/spec/frontend/webhooks/components/__snapshots__/push_events_spec.js.snap
+++ b/spec/frontend/webhooks/components/__snapshots__/push_events_spec.js.snap
@@ -15,9 +15,7 @@ exports[`Webhook push events form editor component Different push events rules w
data-testid="rule_all_branches"
value="all_branches"
>
- <div
- data-qa-selector="strategy_radio_all"
- >
+ <div>
All branches
</div>
</gl-form-radio-stub>
@@ -26,9 +24,7 @@ exports[`Webhook push events form editor component Different push events rules w
data-testid="rule_wildcard"
value="wildcard"
>
- <div
- data-qa-selector="strategy_radio_wildcard"
- >
+ <div>
Wildcard pattern
</div>
</gl-form-radio-stub>
@@ -40,9 +36,7 @@ exports[`Webhook push events form editor component Different push events rules w
data-testid="rule_regex"
value="regex"
>
- <div
- data-qa-selector="strategy_radio_regex"
- >
+ <div>
Regular expression
</div>
</gl-form-radio-stub>
@@ -67,9 +61,7 @@ exports[`Webhook push events form editor component Different push events rules w
data-testid="rule_all_branches"
value="all_branches"
>
- <div
- data-qa-selector="strategy_radio_all"
- >
+ <div>
All branches
</div>
</gl-form-radio-stub>
@@ -78,9 +70,7 @@ exports[`Webhook push events form editor component Different push events rules w
data-testid="rule_wildcard"
value="wildcard"
>
- <div
- data-qa-selector="strategy_radio_wildcard"
- >
+ <div>
Wildcard pattern
</div>
</gl-form-radio-stub>
@@ -92,9 +82,7 @@ exports[`Webhook push events form editor component Different push events rules w
data-testid="rule_regex"
value="regex"
>
- <div
- data-qa-selector="strategy_radio_regex"
- >
+ <div>
Regular expression
</div>
</gl-form-radio-stub>
@@ -102,7 +90,6 @@ exports[`Webhook push events form editor component Different push events rules w
class="gl-ml-6"
>
<gl-form-input-stub
- data-qa-selector="webhook_branch_filter_field"
data-testid="webhook_branch_filter_field"
name="hook[push_events_branch_filter]"
value="foo"
@@ -133,9 +120,7 @@ exports[`Webhook push events form editor component Different push events rules w
data-testid="rule_all_branches"
value="all_branches"
>
- <div
- data-qa-selector="strategy_radio_all"
- >
+ <div>
All branches
</div>
</gl-form-radio-stub>
@@ -144,9 +129,7 @@ exports[`Webhook push events form editor component Different push events rules w
data-testid="rule_wildcard"
value="wildcard"
>
- <div
- data-qa-selector="strategy_radio_wildcard"
- >
+ <div>
Wildcard pattern
</div>
</gl-form-radio-stub>
@@ -154,7 +137,6 @@ exports[`Webhook push events form editor component Different push events rules w
class="gl-ml-6"
>
<gl-form-input-stub
- data-qa-selector="webhook_branch_filter_field"
data-testid="webhook_branch_filter_field"
name="hook[push_events_branch_filter]"
value="foo"
@@ -172,9 +154,7 @@ exports[`Webhook push events form editor component Different push events rules w
data-testid="rule_regex"
value="regex"
>
- <div
- data-qa-selector="strategy_radio_regex"
- >
+ <div>
Regular expression
</div>
</gl-form-radio-stub>
@@ -199,9 +179,7 @@ exports[`Webhook push events form editor component Different push events rules w
data-testid="rule_all_branches"
value="all_branches"
>
- <div
- data-qa-selector="strategy_radio_all"
- >
+ <div>
All branches
</div>
</gl-form-radio-stub>
@@ -210,9 +188,7 @@ exports[`Webhook push events form editor component Different push events rules w
data-testid="rule_wildcard"
value="wildcard"
>
- <div
- data-qa-selector="strategy_radio_wildcard"
- >
+ <div>
Wildcard pattern
</div>
</gl-form-radio-stub>
@@ -224,9 +200,7 @@ exports[`Webhook push events form editor component Different push events rules w
data-testid="rule_regex"
value="regex"
>
- <div
- data-qa-selector="strategy_radio_regex"
- >
+ <div>
Regular expression
</div>
</gl-form-radio-stub>
@@ -251,9 +225,7 @@ exports[`Webhook push events form editor component Different push events rules w
data-testid="rule_all_branches"
value="all_branches"
>
- <div
- data-qa-selector="strategy_radio_all"
- >
+ <div>
All branches
</div>
</gl-form-radio-stub>
@@ -262,9 +234,7 @@ exports[`Webhook push events form editor component Different push events rules w
data-testid="rule_wildcard"
value="wildcard"
>
- <div
- data-qa-selector="strategy_radio_wildcard"
- >
+ <div>
Wildcard pattern
</div>
</gl-form-radio-stub>
@@ -276,9 +246,7 @@ exports[`Webhook push events form editor component Different push events rules w
data-testid="rule_regex"
value="regex"
>
- <div
- data-qa-selector="strategy_radio_regex"
- >
+ <div>
Regular expression
</div>
</gl-form-radio-stub>
@@ -286,7 +254,6 @@ exports[`Webhook push events form editor component Different push events rules w
class="gl-ml-6"
>
<gl-form-input-stub
- data-qa-selector="webhook_branch_filter_field"
data-testid="webhook_branch_filter_field"
name="hook[push_events_branch_filter]"
value=""
@@ -317,9 +284,7 @@ exports[`Webhook push events form editor component Different push events rules w
data-testid="rule_all_branches"
value="all_branches"
>
- <div
- data-qa-selector="strategy_radio_all"
- >
+ <div>
All branches
</div>
</gl-form-radio-stub>
@@ -328,9 +293,7 @@ exports[`Webhook push events form editor component Different push events rules w
data-testid="rule_wildcard"
value="wildcard"
>
- <div
- data-qa-selector="strategy_radio_wildcard"
- >
+ <div>
Wildcard pattern
</div>
</gl-form-radio-stub>
@@ -338,7 +301,6 @@ exports[`Webhook push events form editor component Different push events rules w
class="gl-ml-6"
>
<gl-form-input-stub
- data-qa-selector="webhook_branch_filter_field"
data-testid="webhook_branch_filter_field"
name="hook[push_events_branch_filter]"
value=""
@@ -356,9 +318,7 @@ exports[`Webhook push events form editor component Different push events rules w
data-testid="rule_regex"
value="regex"
>
- <div
- data-qa-selector="strategy_radio_regex"
- >
+ <div>
Regular expression
</div>
</gl-form-radio-stub>
diff --git a/spec/frontend/work_items/components/notes/system_note_spec.js b/spec/frontend/work_items/components/notes/system_note_spec.js
index 03f1aa356ad..69bc0961240 100644
--- a/spec/frontend/work_items/components/notes/system_note_spec.js
+++ b/spec/frontend/work_items/components/notes/system_note_spec.js
@@ -40,8 +40,14 @@ describe('Work Items system note component', () => {
);
});
- it('should render svg icon', () => {
- expect(findTimelineIcon().exists()).toBe(true);
+ it('should render svg icon only for allowed icons', () => {
+ expect(findTimelineIcon().exists()).toBe(false);
+
+ const ALLOWED_ICONS = ['issue-close'];
+ ALLOWED_ICONS.forEach((icon) => {
+ createComponent({ note: { ...workItemSystemNoteWithMetadata, systemNoteIconName: icon } });
+ expect(findTimelineIcon().exists()).toBe(true);
+ });
});
it('should not show compare previous version for FOSS', () => {
diff --git a/spec/frontend/work_items/components/notes/work_item_comment_form_spec.js b/spec/frontend/work_items/components/notes/work_item_comment_form_spec.js
index ee2b434bd75..fe89c525fea 100644
--- a/spec/frontend/work_items/components/notes/work_item_comment_form_spec.js
+++ b/spec/frontend/work_items/components/notes/work_item_comment_form_spec.js
@@ -10,7 +10,7 @@ import { STATE_OPEN } from '~/work_items/constants';
import * as confirmViaGlModal from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal';
import WorkItemCommentForm from '~/work_items/components/notes/work_item_comment_form.vue';
import MarkdownEditor from '~/vue_shared/components/markdown/markdown_editor.vue';
-import WorkItemStateToggleButton from '~/work_items/components/work_item_state_toggle_button.vue';
+import WorkItemStateToggle from '~/work_items/components/work_item_state_toggle.vue';
Vue.use(VueApollo);
@@ -37,7 +37,7 @@ describe('Work item comment form component', () => {
const findConfirmButton = () => wrapper.find('[data-testid="confirm-button"]');
const findInternalNoteCheckbox = () => wrapper.findComponent(GlFormCheckbox);
const findInternalNoteTooltipIcon = () => wrapper.findComponent(GlIcon);
- const findWorkItemToggleStateButton = () => wrapper.findComponent(WorkItemStateToggleButton);
+ const findWorkItemToggleStateButton = () => wrapper.findComponent(WorkItemStateToggle);
const createComponent = ({
isSubmitting = false,
diff --git a/spec/frontend/work_items/components/notes/work_item_note_actions_spec.js b/spec/frontend/work_items/components/notes/work_item_note_actions_spec.js
index 6a24987b737..596283a9590 100644
--- a/spec/frontend/work_items/components/notes/work_item_note_actions_spec.js
+++ b/spec/frontend/work_items/components/notes/work_item_note_actions_spec.js
@@ -1,4 +1,4 @@
-import { GlDisclosureDropdown } from '@gitlab/ui';
+import { GlButton, GlDisclosureDropdown } from '@gitlab/ui';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
@@ -17,7 +17,7 @@ describe('Work Item Note Actions', () => {
const showSpy = jest.fn();
const findReplyButton = () => wrapper.findComponent(ReplyButton);
- const findEditButton = () => wrapper.findByTestId('edit-work-item-note');
+ const findEditButton = () => wrapper.findComponent(GlButton);
const findEmojiButton = () => wrapper.findByTestId('note-emoji-button');
const findDropdown = () => wrapper.findComponent(GlDisclosureDropdown);
const findDeleteNoteButton = () => wrapper.findByTestId('delete-note-action');
diff --git a/spec/frontend/work_items/components/shared/work_item_link_child_contents_spec.js b/spec/frontend/work_items/components/shared/work_item_link_child_contents_spec.js
index 2e1a7983dec..a40e860d9fe 100644
--- a/spec/frontend/work_items/components/shared/work_item_link_child_contents_spec.js
+++ b/spec/frontend/work_items/components/shared/work_item_link_child_contents_spec.js
@@ -1,4 +1,4 @@
-import { GlLabel, GlIcon, GlLink } from '@gitlab/ui';
+import { GlLabel, GlIcon, GlLink, GlButton } from '@gitlab/ui';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
@@ -9,7 +9,6 @@ import { createAlert } from '~/alert';
import RichTimestampTooltip from '~/vue_shared/components/rich_timestamp_tooltip.vue';
import WorkItemLinkChildContents from '~/work_items/components/shared/work_item_link_child_contents.vue';
-import WorkItemLinksMenu from '~/work_items/components/shared/work_item_links_menu.vue';
import { WORK_ITEM_TYPE_VALUE_OBJECTIVE } from '~/work_items/constants';
import {
@@ -39,13 +38,18 @@ describe('WorkItemLinkChildContents', () => {
const findAllLabels = () => wrapper.findAllComponents(GlLabel);
const findRegularLabel = () => findAllLabels().at(0);
const findScopedLabel = () => findAllLabels().at(1);
- const findLinksMenuComponent = () => wrapper.findComponent(WorkItemLinksMenu);
+ const findRemoveButton = () => wrapper.findComponent(GlButton);
- const createComponent = ({ canUpdate = true, childItem = workItemTask } = {}) => {
+ const createComponent = ({
+ canUpdate = true,
+ childItem = workItemTask,
+ showLabels = true,
+ } = {}) => {
wrapper = shallowMountExtended(WorkItemLinkChildContents, {
propsData: {
canUpdate,
childItem,
+ showLabels,
},
});
};
@@ -129,19 +133,6 @@ describe('WorkItemLinkChildContents', () => {
expect(findMetadataComponent().exists()).toBe(false);
});
-
- it('renders labels', () => {
- const mockLabel = mockLabels[0];
-
- expect(findAllLabels()).toHaveLength(mockLabels.length);
- expect(findRegularLabel().props()).toMatchObject({
- title: mockLabel.title,
- backgroundColor: mockLabel.color,
- description: mockLabel.description,
- scoped: false,
- });
- expect(findScopedLabel().props('scoped')).toBe(true); // Second label is scoped
- });
});
describe('item menu', () => {
@@ -149,20 +140,47 @@ describe('WorkItemLinkChildContents', () => {
createComponent();
});
- it('renders work-item-links-menu', () => {
- expect(findLinksMenuComponent().exists()).toBe(true);
+ it('renders remove button', () => {
+ expect(findRemoveButton().exists()).toBe(true);
});
it('does not render work-item-links-menu when canUpdate is false', () => {
createComponent({ canUpdate: false });
- expect(findLinksMenuComponent().exists()).toBe(false);
+ expect(findRemoveButton().exists()).toBe(false);
});
it('removeChild event on menu triggers `click-remove-child` event', () => {
- findLinksMenuComponent().vm.$emit('removeChild');
+ findRemoveButton().vm.$emit('click');
expect(wrapper.emitted('removeChild')).toEqual([[workItemTask]]);
});
});
+
+ describe('item labels', () => {
+ it('renders normal and scoped label', () => {
+ createComponent({ childItem: workItemObjectiveWithChild });
+
+ const mockLabel = mockLabels[0];
+
+ expect(findAllLabels()).toHaveLength(mockLabels.length);
+ expect(findRegularLabel().props()).toMatchObject({
+ title: mockLabel.title,
+ backgroundColor: mockLabel.color,
+ description: mockLabel.description,
+ scoped: false,
+ });
+ expect(findScopedLabel().props('scoped')).toBe(true); // Second label is scoped
+ });
+
+ it.each`
+ expectedAssertion | showLabels
+ ${'does not render labels'} | ${true}
+ ${'renders label'} | ${false}
+ `('$expectedAssertion when showLabels is $showLabels', ({ showLabels }) => {
+ createComponent({ showLabels, childItem: workItemObjectiveWithChild });
+
+ expect(findAllLabels().exists()).toBe(showLabels);
+ });
+ });
});
diff --git a/spec/frontend/work_items/components/shared/work_item_links_menu_spec.js b/spec/frontend/work_items/components/shared/work_item_links_menu_spec.js
deleted file mode 100644
index 338a70feae4..00000000000
--- a/spec/frontend/work_items/components/shared/work_item_links_menu_spec.js
+++ /dev/null
@@ -1,30 +0,0 @@
-import { GlDisclosureDropdown, GlDisclosureDropdownItem } from '@gitlab/ui';
-
-import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import WorkItemLinksMenu from '~/work_items/components/shared/work_item_links_menu.vue';
-
-describe('WorkItemLinksMenu', () => {
- let wrapper;
-
- const createComponent = () => {
- wrapper = shallowMountExtended(WorkItemLinksMenu);
- };
-
- const findDropdown = () => wrapper.findComponent(GlDisclosureDropdown);
- const findRemoveDropdownItem = () => wrapper.findComponent(GlDisclosureDropdownItem);
-
- beforeEach(() => {
- createComponent();
- });
-
- it('renders dropdown and dropdown items', () => {
- expect(findDropdown().exists()).toBe(true);
- expect(findRemoveDropdownItem().exists()).toBe(true);
- });
-
- it('emits removeChild event on click Remove', () => {
- findRemoveDropdownItem().vm.$emit('action');
-
- expect(wrapper.emitted('removeChild')).toHaveLength(1);
- });
-});
diff --git a/spec/frontend/work_items/components/shared/work_item_token_input_spec.js b/spec/frontend/work_items/components/shared/work_item_token_input_spec.js
index 075b69415cf..5726aaaa2d0 100644
--- a/spec/frontend/work_items/components/shared/work_item_token_input_spec.js
+++ b/spec/frontend/work_items/components/shared/work_item_token_input_spec.js
@@ -1,13 +1,19 @@
-import Vue from 'vue';
-import { GlTokenSelector } from '@gitlab/ui';
+import Vue, { nextTick } from 'vue';
+import { GlTokenSelector, GlAlert } from '@gitlab/ui';
import VueApollo from 'vue-apollo';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import WorkItemTokenInput from '~/work_items/components/shared/work_item_token_input.vue';
import { WORK_ITEM_TYPE_ENUM_TASK } from '~/work_items/constants';
+import groupWorkItemsQuery from '~/work_items/graphql/group_work_items.query.graphql';
import projectWorkItemsQuery from '~/work_items/graphql/project_work_items.query.graphql';
-import { availableWorkItemsResponse, searchedWorkItemsResponse } from '../../mock_data';
+import {
+ availableWorkItemsResponse,
+ searchWorkItemsTextResponse,
+ searchWorkItemsIidResponse,
+ searchWorkItemsTextIidResponse,
+} from '../../mock_data';
Vue.use(VueApollo);
@@ -15,17 +21,27 @@ describe('WorkItemTokenInput', () => {
let wrapper;
const availableWorkItemsResolver = jest.fn().mockResolvedValue(availableWorkItemsResponse);
- const searchedWorkItemResolver = jest.fn().mockResolvedValue(searchedWorkItemsResponse);
+ const groupSearchedWorkItemResolver = jest.fn().mockResolvedValue(searchWorkItemsTextResponse);
+ const searchWorkItemTextResolver = jest.fn().mockResolvedValue(searchWorkItemsTextResponse);
+ const searchWorkItemIidResolver = jest.fn().mockResolvedValue(searchWorkItemsIidResponse);
+ const searchWorkItemTextIidResolver = jest.fn().mockResolvedValue(searchWorkItemsTextIidResponse);
const createComponent = async ({
workItemsToAdd = [],
parentConfidential = false,
childrenType = WORK_ITEM_TYPE_ENUM_TASK,
areWorkItemsToAddValid = true,
- workItemsResolver = searchedWorkItemResolver,
+ workItemsResolver = searchWorkItemTextResolver,
+ isGroup = false,
} = {}) => {
wrapper = shallowMountExtended(WorkItemTokenInput, {
- apolloProvider: createMockApollo([[projectWorkItemsQuery, workItemsResolver]]),
+ apolloProvider: createMockApollo([
+ [projectWorkItemsQuery, workItemsResolver],
+ [groupWorkItemsQuery, groupSearchedWorkItemResolver],
+ ]),
+ provide: {
+ isGroup,
+ },
propsData: {
value: workItemsToAdd,
childrenType,
@@ -41,6 +57,7 @@ describe('WorkItemTokenInput', () => {
};
const findTokenSelector = () => wrapper.findComponent(GlTokenSelector);
+ const findGlAlert = () => wrapper.findComponent(GlAlert);
it('searches for available work items on focus', async () => {
createComponent({ workItemsResolver: availableWorkItemsResolver });
@@ -52,24 +69,34 @@ describe('WorkItemTokenInput', () => {
searchTerm: '',
types: [WORK_ITEM_TYPE_ENUM_TASK],
in: undefined,
+ iid: null,
+ isNumber: false,
});
expect(findTokenSelector().props('dropdownItems')).toHaveLength(3);
});
- it('searches for available work items when typing in input', async () => {
- createComponent({ workItemsResolver: searchedWorkItemResolver });
- findTokenSelector().vm.$emit('focus');
- findTokenSelector().vm.$emit('text-input', 'Task 2');
- await waitForPromises();
+ it.each`
+ inputType | input | resolver | searchTerm | iid | isNumber | length
+ ${'iid'} | ${'101'} | ${searchWorkItemIidResolver} | ${'101'} | ${'101'} | ${true} | ${1}
+ ${'text'} | ${'Task 2'} | ${searchWorkItemTextResolver} | ${'Task 2'} | ${null} | ${false} | ${1}
+ ${'iid and text'} | ${'123'} | ${searchWorkItemTextIidResolver} | ${'123'} | ${'123'} | ${true} | ${2}
+ `(
+ 'searches by $inputType for available work items when typing in input',
+ async ({ input, resolver, searchTerm, iid, isNumber, length }) => {
+ createComponent({ workItemsResolver: resolver });
+ findTokenSelector().vm.$emit('focus');
+ findTokenSelector().vm.$emit('text-input', input);
+ await waitForPromises();
- expect(searchedWorkItemResolver).toHaveBeenCalledWith({
- fullPath: 'test-project-path',
- searchTerm: 'Task 2',
- types: [WORK_ITEM_TYPE_ENUM_TASK],
- in: 'TITLE',
- });
- expect(findTokenSelector().props('dropdownItems')).toHaveLength(1);
- });
+ expect(resolver).toHaveBeenCalledWith({
+ searchTerm,
+ in: 'TITLE',
+ iid,
+ isNumber,
+ });
+ expect(findTokenSelector().props('dropdownItems')).toHaveLength(length);
+ },
+ );
it('renders red border around token selector input when work item is not valid', () => {
createComponent({
@@ -78,4 +105,58 @@ describe('WorkItemTokenInput', () => {
expect(findTokenSelector().props('containerClass')).toBe('gl-inset-border-1-red-500!');
});
+
+ describe('when project context', () => {
+ beforeEach(() => {
+ createComponent();
+ findTokenSelector().vm.$emit('focus');
+ });
+
+ it('calls the project work items query', () => {
+ expect(searchWorkItemTextResolver).toHaveBeenCalled();
+ });
+
+ it('skips calling the group work items query', () => {
+ expect(groupSearchedWorkItemResolver).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('when group context', () => {
+ beforeEach(() => {
+ createComponent({ isGroup: true });
+ findTokenSelector().vm.$emit('focus');
+ });
+
+ it('skips calling the project work items query', () => {
+ expect(searchWorkItemTextResolver).not.toHaveBeenCalled();
+ });
+
+ it('calls the group work items query', () => {
+ expect(groupSearchedWorkItemResolver).toHaveBeenCalled();
+ });
+ });
+
+ describe('when project work items query fails', () => {
+ beforeEach(() => {
+ createComponent({
+ workItemsResolver: jest
+ .fn()
+ .mockRejectedValue('Something went wrong while fetching the results'),
+ });
+ findTokenSelector().vm.$emit('focus');
+ });
+
+ it('shows error and allows error alert to be closed', async () => {
+ await waitForPromises();
+ expect(findGlAlert().exists()).toBe(true);
+ expect(findGlAlert().text()).toBe(
+ 'Something went wrong while fetching the task. Please try again.',
+ );
+
+ findGlAlert().vm.$emit('dismiss');
+ await nextTick();
+
+ expect(findGlAlert().exists()).toBe(false);
+ });
+ });
});
diff --git a/spec/frontend/work_items/components/work_item_actions_spec.js b/spec/frontend/work_items/components/work_item_actions_spec.js
index 15c33bf5b1e..6cbb3c4384e 100644
--- a/spec/frontend/work_items/components/work_item_actions_spec.js
+++ b/spec/frontend/work_items/components/work_item_actions_spec.js
@@ -1,4 +1,10 @@
-import { GlDisclosureDropdown, GlDropdownDivider, GlModal, GlToggle } from '@gitlab/ui';
+import {
+ GlDisclosureDropdown,
+ GlDropdownDivider,
+ GlModal,
+ GlToggle,
+ GlDisclosureDropdownItem,
+} from '@gitlab/ui';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
@@ -10,14 +16,16 @@ import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { isLoggedIn } from '~/lib/utils/common_utils';
import toast from '~/vue_shared/plugins/global_toast';
import WorkItemActions from '~/work_items/components/work_item_actions.vue';
+import WorkItemStateToggle from '~/work_items/components/work_item_state_toggle.vue';
import {
+ STATE_OPEN,
TEST_ID_CONFIDENTIALITY_TOGGLE_ACTION,
- TEST_ID_NOTIFICATIONS_TOGGLE_ACTION,
TEST_ID_NOTIFICATIONS_TOGGLE_FORM,
TEST_ID_DELETE_ACTION,
TEST_ID_PROMOTE_ACTION,
TEST_ID_COPY_REFERENCE_ACTION,
TEST_ID_COPY_CREATE_NOTE_EMAIL_ACTION,
+ TEST_ID_TOGGLE_ACTION,
} from '~/work_items/constants';
import updateWorkItemNotificationsMutation from '~/work_items/graphql/update_work_item_notifications.mutation.graphql';
import projectWorkItemTypesQuery from '~/work_items/graphql/project_work_item_types.query.graphql';
@@ -44,11 +52,10 @@ describe('WorkItemActions component', () => {
const findModal = () => wrapper.findComponent(GlModal);
const findConfidentialityToggleButton = () =>
wrapper.findByTestId(TEST_ID_CONFIDENTIALITY_TOGGLE_ACTION);
- const findNotificationsToggleButton = () =>
- wrapper.findByTestId(TEST_ID_NOTIFICATIONS_TOGGLE_ACTION);
const findDeleteButton = () => wrapper.findByTestId(TEST_ID_DELETE_ACTION);
const findPromoteButton = () => wrapper.findByTestId(TEST_ID_PROMOTE_ACTION);
const findCopyReferenceButton = () => wrapper.findByTestId(TEST_ID_COPY_REFERENCE_ACTION);
+ const findWorkItemToggleOption = () => wrapper.findComponent(WorkItemStateToggle);
const findCopyCreateNoteEmailButton = () =>
wrapper.findByTestId(TEST_ID_COPY_CREATE_NOTE_EMAIL_ACTION);
const findDropdownItems = () => wrapper.findAll('[data-testid="work-item-actions-dropdown"] > *');
@@ -108,6 +115,7 @@ describe('WorkItemActions component', () => {
[updateWorkItemNotificationsMutation, notificationsMutationHandler],
]),
propsData: {
+ workItemState: STATE_OPEN,
fullPath: 'gitlab-org/gitlab-test',
workItemId: 'gid://gitlab/WorkItem/1',
canUpdate,
@@ -132,6 +140,7 @@ describe('WorkItemActions component', () => {
show: jest.fn(),
},
}),
+ GlDisclosureDropdownItem,
GlDisclosureDropdown: stubComponent(GlDisclosureDropdown, {
methods: {
close: modalShowSpy,
@@ -167,6 +176,10 @@ describe('WorkItemActions component', () => {
text: 'Turn on confidentiality',
},
{
+ testId: TEST_ID_TOGGLE_ACTION,
+ text: '',
+ },
+ {
testId: TEST_ID_COPY_REFERENCE_ACTION,
text: 'Copy reference',
},
@@ -248,7 +261,7 @@ describe('WorkItemActions component', () => {
});
it('renders toggle button', () => {
- expect(findNotificationsToggleButton().exists()).toBe(true);
+ expect(findNotificationsToggle().exists()).toBe(true);
});
it.each`
@@ -366,4 +379,10 @@ describe('WorkItemActions component', () => {
expect(toast).toHaveBeenCalledWith('Email address copied');
});
});
+
+ it('renders the toggle status button', () => {
+ createComponent();
+
+ expect(findWorkItemToggleOption().exists()).toBe(true);
+ });
});
diff --git a/spec/frontend/work_items/components/work_item_award_emoji_spec.js b/spec/frontend/work_items/components/work_item_award_emoji_spec.js
index f8c5f8edc4c..a756bfa6889 100644
--- a/spec/frontend/work_items/components/work_item_award_emoji_spec.js
+++ b/spec/frontend/work_items/components/work_item_award_emoji_spec.js
@@ -9,7 +9,8 @@ import { isLoggedIn } from '~/lib/utils/common_utils';
import AwardList from '~/vue_shared/components/awards_list.vue';
import WorkItemAwardEmoji from '~/work_items/components/work_item_award_emoji.vue';
import updateAwardEmojiMutation from '~/work_items/graphql/update_award_emoji.mutation.graphql';
-import workItemAwardEmojiQuery from '~/work_items/graphql/award_emoji.query.graphql';
+import groupWorkItemAwardEmojiQuery from '~/work_items/graphql/group_award_emoji.query.graphql';
+import projectWorkItemAwardEmojiQuery from '~/work_items/graphql/award_emoji.query.graphql';
import {
EMOJI_THUMBSUP,
EMOJI_THUMBSDOWN,
@@ -42,6 +43,7 @@ describe('WorkItemAwardEmoji component', () => {
const workItemQueryResponse = workItemByIidResponseFactory();
const mockWorkItem = workItemQueryResponse.data.workspace.workItems.nodes[0];
+ const groupAwardEmojiQuerySuccessHandler = jest.fn().mockResolvedValue(workItemQueryResponse);
const awardEmojiQuerySuccessHandler = jest.fn().mockResolvedValue(workItemQueryResponse);
const awardEmojiQueryEmptyHandler = jest.fn().mockResolvedValue(
workItemByIidResponseFactory({
@@ -83,10 +85,12 @@ describe('WorkItemAwardEmoji component', () => {
awardEmojiQueryHandler = awardEmojiQuerySuccessHandler,
awardEmojiMutationHandler = awardEmojiAddSuccessHandler,
workItemIid = '1',
+ isGroup = false,
} = {}) => {
mockApolloProvider = createMockApollo(
[
- [workItemAwardEmojiQuery, awardEmojiQueryHandler],
+ [projectWorkItemAwardEmojiQuery, awardEmojiQueryHandler],
+ [groupWorkItemAwardEmojiQuery, groupAwardEmojiQuerySuccessHandler],
[updateAwardEmojiMutation, awardEmojiMutationHandler],
],
{},
@@ -108,6 +112,9 @@ describe('WorkItemAwardEmoji component', () => {
wrapper = shallowMount(WorkItemAwardEmoji, {
isLoggedIn: isLoggedIn(),
apolloProvider: mockApolloProvider,
+ provide: {
+ isGroup,
+ },
propsData: {
workItemId: 'gid://gitlab/WorkItem/1',
workItemFullpath: 'test-project-path',
@@ -270,7 +277,7 @@ describe('WorkItemAwardEmoji component', () => {
};
});
- it('calls mutation succesfully and adds the award emoji with proper user details', async () => {
+ it('calls mutation successfully and adds the award emoji with proper user details', async () => {
createComponent({
awardEmojiMutationHandler: awardEmojiAddSuccessHandler,
});
@@ -345,4 +352,18 @@ describe('WorkItemAwardEmoji component', () => {
});
});
});
+
+ describe('group award emoji query', () => {
+ it('is not called in a project context', () => {
+ createComponent();
+
+ expect(groupAwardEmojiQuerySuccessHandler).not.toHaveBeenCalled();
+ });
+
+ it('is called in a group context', () => {
+ createComponent({ isGroup: true });
+
+ expect(groupAwardEmojiQuerySuccessHandler).toHaveBeenCalled();
+ });
+ });
});
diff --git a/spec/frontend/work_items/components/work_item_description_spec.js b/spec/frontend/work_items/components/work_item_description_spec.js
index de2895591dd..1d25bb74986 100644
--- a/spec/frontend/work_items/components/work_item_description_spec.js
+++ b/spec/frontend/work_items/components/work_item_description_spec.js
@@ -92,7 +92,7 @@ describe('WorkItemDescription', () => {
it('passes correct autocompletion data and preview markdown sources and enables quick actions', async () => {
const {
iid,
- project: { fullPath },
+ namespace: { fullPath },
} = workItemQueryResponse.data.workItem;
await createComponent({ isEditing: true });
diff --git a/spec/frontend/work_items/components/work_item_detail_spec.js b/spec/frontend/work_items/components/work_item_detail_spec.js
index 28826748cb0..acfe4571cd2 100644
--- a/spec/frontend/work_items/components/work_item_detail_spec.js
+++ b/spec/frontend/work_items/components/work_item_detail_spec.js
@@ -23,8 +23,6 @@ import WorkItemTree from '~/work_items/components/work_item_links/work_item_tree
import WorkItemRelationships from '~/work_items/components/work_item_relationships/work_item_relationships.vue';
import WorkItemNotes from '~/work_items/components/work_item_notes.vue';
import WorkItemDetailModal from '~/work_items/components/work_item_detail_modal.vue';
-import WorkItemTypeIcon from '~/work_items/components/work_item_type_icon.vue';
-import WorkItemStateToggleButton from '~/work_items/components/work_item_state_toggle_button.vue';
import AbuseCategorySelector from '~/abuse_reports/components/abuse_category_selector.vue';
import WorkItemTodos from '~/work_items/components/work_item_todos.vue';
import { i18n } from '~/work_items/constants';
@@ -55,10 +53,6 @@ describe('WorkItemDetail component', () => {
canUpdate: true,
canDelete: true,
});
- const workItemQueryResponseWithCannotUpdate = workItemByIidResponseFactory({
- canUpdate: false,
- canDelete: false,
- });
const workItemQueryResponseWithoutParent = workItemByIidResponseFactory({
parent: null,
canUpdate: true,
@@ -95,8 +89,6 @@ describe('WorkItemDetail component', () => {
const findWorkItemTwoColumnViewContainer = () => wrapper.findByTestId('work-item-overview');
const findRightSidebar = () => wrapper.findByTestId('work-item-overview-right-sidebar');
const triggerPageScroll = () => findIntersectionObserver().vm.$emit('disappear');
- const findWorkItemStateToggleButton = () => wrapper.findComponent(WorkItemStateToggleButton);
- const findWorkItemTypeIcon = () => wrapper.findComponent(WorkItemTypeIcon);
const createComponent = ({
isGroup = false,
@@ -212,25 +204,6 @@ describe('WorkItemDetail component', () => {
});
});
- describe('work item state toggle button', () => {
- describe.each`
- description | canUpdate
- ${'when user cannot update'} | ${false}
- ${'when user can update'} | ${true}
- `('$description', ({ canUpdate }) => {
- it(`${canUpdate ? 'is rendered' : 'is not rendered'}`, async () => {
- createComponent({
- handler: canUpdate
- ? jest.fn().mockResolvedValue(workItemQueryResponse)
- : jest.fn().mockResolvedValue(workItemQueryResponseWithCannotUpdate),
- });
- await waitForPromises();
-
- expect(findWorkItemStateToggleButton().exists()).toBe(canUpdate);
- });
- });
- });
-
describe('close button', () => {
describe('when isModal prop is false', () => {
it('does not render', async () => {
@@ -408,12 +381,11 @@ describe('WorkItemDetail component', () => {
expect(findParent().exists()).toBe(false);
});
- it('shows work item type with reference when there is no a parent', async () => {
+ it('shows title in the header when there is no parent', async () => {
createComponent({ handler: jest.fn().mockResolvedValue(workItemQueryResponseWithoutParent) });
await waitForPromises();
- expect(findWorkItemTypeIcon().props('showText')).toBe(true);
- expect(findWorkItemType().text()).toBe('#1');
+ expect(findWorkItemType().classes()).toEqual(['gl-w-full']);
});
describe('with parent', () => {
@@ -428,10 +400,6 @@ describe('WorkItemDetail component', () => {
expect(findParent().exists()).toBe(true);
});
- it('does not show work item type', () => {
- expect(findWorkItemType().exists()).toBe(false);
- });
-
it('shows parent breadcrumb icon', () => {
expect(findParentButton().props('icon')).toBe(mockParent.parent.workItemType.iconName);
});
@@ -468,6 +436,10 @@ describe('WorkItemDetail component', () => {
const { iid } = workItemQueryResponse.data.workspace.workItems.nodes[0];
expect(findParent().text()).toContain(`#${iid}`);
});
+
+ it('does not show title in the header when parent exists', () => {
+ expect(findWorkItemType().classes()).toEqual(['gl-sm-display-none!']);
+ });
});
});
diff --git a/spec/frontend/work_items/components/work_item_labels_spec.js b/spec/frontend/work_items/components/work_item_labels_spec.js
index 28aa7ffa1be..d7bebac6dbd 100644
--- a/spec/frontend/work_items/components/work_item_labels_spec.js
+++ b/spec/frontend/work_items/components/work_item_labels_spec.js
@@ -5,7 +5,8 @@ import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
-import labelSearchQuery from '~/sidebar/components/labels/labels_select_widget/graphql/project_labels.query.graphql';
+import groupLabelsQuery from '~/sidebar/components/labels/labels_select_widget/graphql/group_labels.query.graphql';
+import projectLabelsQuery from '~/sidebar/components/labels/labels_select_widget/graphql/project_labels.query.graphql';
import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql';
import groupWorkItemByIidQuery from '~/work_items/graphql/group_work_item_by_iid.query.graphql';
import workItemByIidQuery from '~/work_items/graphql/work_item_by_iid.query.graphql';
@@ -37,7 +38,8 @@ describe('WorkItemLabels component', () => {
const groupWorkItemQuerySuccess = jest
.fn()
.mockResolvedValue(groupWorkItemByIidResponseFactory({ labels: null }));
- const successSearchQueryHandler = jest.fn().mockResolvedValue(projectLabelsResponse);
+ const projectLabelsQueryHandler = jest.fn().mockResolvedValue(projectLabelsResponse);
+ const groupLabelsQueryHandler = jest.fn().mockResolvedValue(projectLabelsResponse);
const successUpdateWorkItemMutationHandler = jest
.fn()
.mockResolvedValue(updateWorkItemMutationResponse);
@@ -47,7 +49,7 @@ describe('WorkItemLabels component', () => {
canUpdate = true,
isGroup = false,
workItemQueryHandler = workItemQuerySuccess,
- searchQueryHandler = successSearchQueryHandler,
+ searchQueryHandler = projectLabelsQueryHandler,
updateWorkItemMutationHandler = successUpdateWorkItemMutationHandler,
workItemIid = '1',
} = {}) => {
@@ -55,7 +57,8 @@ describe('WorkItemLabels component', () => {
apolloProvider: createMockApollo([
[workItemByIidQuery, workItemQueryHandler],
[groupWorkItemByIidQuery, groupWorkItemQuerySuccess],
- [labelSearchQuery, searchQueryHandler],
+ [projectLabelsQuery, searchQueryHandler],
+ [groupLabelsQuery, groupLabelsQueryHandler],
[updateWorkItemMutation, updateWorkItemMutationHandler],
]),
provide: {
@@ -179,7 +182,7 @@ describe('WorkItemLabels component', () => {
findTokenSelector().vm.$emit('text-input', searchKey);
await waitForPromises();
- expect(successSearchQueryHandler).toHaveBeenCalledWith(
+ expect(projectLabelsQueryHandler).toHaveBeenCalledWith(
expect.objectContaining({ searchTerm: searchKey }),
);
});
@@ -273,6 +276,16 @@ describe('WorkItemLabels component', () => {
expect(workItemQuerySuccess).not.toHaveBeenCalled();
});
+
+ it('calls the project labels query on search', async () => {
+ createComponent();
+
+ findTokenSelector().vm.$emit('focus');
+ findTokenSelector().vm.$emit('text-input', 'hello');
+ await waitForPromises();
+
+ expect(projectLabelsQueryHandler).toHaveBeenCalled();
+ });
});
describe('when group context', () => {
@@ -296,5 +309,15 @@ describe('WorkItemLabels component', () => {
expect(groupWorkItemQuerySuccess).not.toHaveBeenCalled();
});
+
+ it('calls the group labels query on search', async () => {
+ createComponent({ isGroup: true });
+
+ findTokenSelector().vm.$emit('focus');
+ findTokenSelector().vm.$emit('text-input', 'hello');
+ await waitForPromises();
+
+ expect(groupLabelsQueryHandler).toHaveBeenCalled();
+ });
});
});
diff --git a/spec/frontend/work_items/components/work_item_links/work_item_link_child_spec.js b/spec/frontend/work_items/components/work_item_links/work_item_link_child_spec.js
index 9addf6c3450..36af0c5b3c8 100644
--- a/spec/frontend/work_items/components/work_item_links/work_item_link_child_spec.js
+++ b/spec/frontend/work_items/components/work_item_links/work_item_link_child_spec.js
@@ -91,6 +91,7 @@ describe('WorkItemLinkChild', () => {
childItem: workItemObjectiveWithChild,
canUpdate: true,
showTaskIcon: false,
+ showLabels: true,
});
});
});
diff --git a/spec/frontend/work_items/components/work_item_links/work_item_links_spec.js b/spec/frontend/work_items/components/work_item_links/work_item_links_spec.js
index 0b88b3ff5b4..f8b2736c0f8 100644
--- a/spec/frontend/work_items/components/work_item_links/work_item_links_spec.js
+++ b/spec/frontend/work_items/components/work_item_links/work_item_links_spec.js
@@ -1,5 +1,6 @@
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
+import { GlToggle } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
@@ -93,6 +94,7 @@ describe('WorkItemLinks', () => {
const findWorkItemDetailModal = () => wrapper.findComponent(WorkItemDetailModal);
const findAbuseCategorySelector = () => wrapper.findComponent(AbuseCategorySelector);
const findWorkItemLinkChildrenWrapper = () => wrapper.findComponent(WorkItemChildrenWrapper);
+ const findShowLabelsToggle = () => wrapper.findComponent(GlToggle);
afterEach(() => {
mockApollo = null;
@@ -278,4 +280,21 @@ describe('WorkItemLinks', () => {
expect(groupResponseWithAddChildPermission).toHaveBeenCalled();
});
});
+
+ it.each`
+ toggleValue
+ ${true}
+ ${false}
+ `(
+ 'passes showLabels as $toggleValue to child items when toggle is $toggleValue',
+ async ({ toggleValue }) => {
+ await createComponent();
+
+ findShowLabelsToggle().vm.$emit('change', toggleValue);
+
+ await nextTick();
+
+ expect(findWorkItemLinkChildrenWrapper().props('showLabels')).toBe(toggleValue);
+ },
+ );
});
diff --git a/spec/frontend/work_items/components/work_item_links/work_item_tree_spec.js b/spec/frontend/work_items/components/work_item_links/work_item_tree_spec.js
index f30fded0b45..6c1d1035c3d 100644
--- a/spec/frontend/work_items/components/work_item_links/work_item_tree_spec.js
+++ b/spec/frontend/work_items/components/work_item_links/work_item_tree_spec.js
@@ -1,4 +1,5 @@
import { nextTick } from 'vue';
+import { GlToggle } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import WidgetWrapper from '~/work_items/components/widget_wrapper.vue';
import WorkItemTree from '~/work_items/components/work_item_links/work_item_tree.vue';
@@ -20,6 +21,7 @@ describe('WorkItemTree', () => {
const findForm = () => wrapper.findComponent(WorkItemLinksForm);
const findWidgetWrapper = () => wrapper.findComponent(WidgetWrapper);
const findWorkItemLinkChildrenWrapper = () => wrapper.findComponent(WorkItemChildrenWrapper);
+ const findShowLabelsToggle = () => wrapper.findComponent(GlToggle);
const createComponent = ({
workItemType = 'Objective',
@@ -126,4 +128,21 @@ describe('WorkItemTree', () => {
expect(wrapper.emitted('addChild')).toEqual([[]]);
});
+
+ it.each`
+ toggleValue
+ ${true}
+ ${false}
+ `(
+ 'passes showLabels as $toggleValue to child items when toggle is $toggleValue',
+ async ({ toggleValue }) => {
+ createComponent();
+
+ findShowLabelsToggle().vm.$emit('change', toggleValue);
+
+ await nextTick();
+
+ expect(findWorkItemLinkChildrenWrapper().props('showLabels')).toBe(toggleValue);
+ },
+ );
});
diff --git a/spec/frontend/work_items/components/work_item_milestone_spec.js b/spec/frontend/work_items/components/work_item_milestone_spec.js
index e303ad4b481..fc2c5eb2af2 100644
--- a/spec/frontend/work_items/components/work_item_milestone_spec.js
+++ b/spec/frontend/work_items/components/work_item_milestone_spec.js
@@ -1,14 +1,8 @@
-import {
- GlDropdown,
- GlDropdownItem,
- GlSearchBoxByType,
- GlSkeletonLoader,
- GlFormGroup,
- GlDropdownText,
-} from '@gitlab/ui';
+import { GlCollapsibleListbox, GlListboxItem, GlSkeletonLoader, GlFormGroup } from '@gitlab/ui';
+
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
-import WorkItemMilestone from '~/work_items/components/work_item_milestone.vue';
+import WorkItemMilestone, { noMilestoneId } from '~/work_items/components/work_item_milestone.vue';
import createMockApollo from 'helpers/mock_apollo_helper';
import { mockTracking } from 'helpers/tracking_helper';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
@@ -32,17 +26,13 @@ describe('WorkItemMilestone component', () => {
const workItemId = 'gid://gitlab/WorkItem/1';
const workItemType = 'Task';
- const findDropdown = () => wrapper.findComponent(GlDropdown);
- const findSearchBox = () => wrapper.findComponent(GlSearchBoxByType);
+ const findDropdown = () => wrapper.findComponent(GlCollapsibleListbox);
const findSkeletonLoader = () => wrapper.findComponent(GlSkeletonLoader);
- const findNoMilestoneDropdownItem = () => wrapper.findByTestId('no-milestone');
- const findDropdownItems = () => wrapper.findAllComponents(GlDropdownItem);
- const findFirstDropdownItem = () => findDropdownItems().at(0);
- const findDropdownTexts = () => wrapper.findAllComponents(GlDropdownText);
- const findDropdownItemAtIndex = (index) => findDropdownItems().at(index);
+ const findNoMilestoneDropdownItem = () => wrapper.findByTestId('listbox-item-no-milestone-id');
+ const findDropdownItems = () => wrapper.findAllComponents(GlListboxItem);
const findDisabledTextSpan = () => wrapper.findByTestId('disabled-text');
- const findDropdownTextAtIndex = (index) => findDropdownTexts().at(index);
const findInputGroup = () => wrapper.findComponent(GlFormGroup);
+ const findNoResultsText = () => wrapper.findByTestId('no-results-text');
const successSearchQueryHandler = jest.fn().mockResolvedValue(projectMilestonesResponse);
const successSearchWithNoMatchingMilestones = jest
@@ -74,8 +64,7 @@ describe('WorkItemMilestone component', () => {
workItemType,
},
stubs: {
- GlDropdown,
- GlSearchBoxByType,
+ GlCollapsibleListbox,
},
});
};
@@ -106,7 +95,7 @@ describe('WorkItemMilestone component', () => {
it(`has a value of "Add to milestone"`, () => {
createComponent({ canUpdate: true, milestone: null });
- expect(findDropdown().props('text')).toBe(WorkItemMilestone.i18n.MILESTONE_PLACEHOLDER);
+ expect(findDropdown().props('toggleText')).toBe(WorkItemMilestone.i18n.MILESTONE_PLACEHOLDER);
});
});
@@ -114,7 +103,7 @@ describe('WorkItemMilestone component', () => {
it('has the search box', () => {
createComponent();
- expect(findSearchBox().exists()).toBe(true);
+ expect(findDropdown().props('searchable')).toBe(true);
});
it('shows no matching results when no items', () => {
@@ -122,9 +111,8 @@ describe('WorkItemMilestone component', () => {
searchQueryHandler: successSearchWithNoMatchingMilestones,
});
- expect(findDropdownTextAtIndex(0).text()).toBe(WorkItemMilestone.i18n.NO_MATCHING_RESULTS);
+ expect(findNoResultsText().text()).toBe(WorkItemMilestone.i18n.NO_MATCHING_RESULTS);
expect(findDropdownItems()).toHaveLength(1);
- expect(findDropdownTexts()).toHaveLength(1);
});
});
@@ -165,16 +153,18 @@ describe('WorkItemMilestone component', () => {
it('changes the milestone to null when clicked on no milestone', async () => {
showDropdown();
- findFirstDropdownItem().vm.$emit('click');
+ findDropdown().vm.$emit('select', noMilestoneId);
hideDropdown();
await nextTick();
expect(findDropdown().props('loading')).toBe(true);
await waitForPromises();
-
- expect(findDropdown().props('loading')).toBe(false);
- expect(findDropdown().props('text')).toBe(WorkItemMilestone.i18n.MILESTONE_PLACEHOLDER);
+ expect(findDropdown().props()).toMatchObject({
+ loading: false,
+ toggleText: WorkItemMilestone.i18n.MILESTONE_PLACEHOLDER,
+ toggleClass: expect.arrayContaining(['gl-text-gray-500!']),
+ });
});
it('changes the milestone to the selected milestone', async () => {
@@ -182,15 +172,16 @@ describe('WorkItemMilestone component', () => {
/** the index is -1 since no matching results is also a dropdown item */
const milestoneAtIndex =
projectMilestonesResponse.data.workspace.attributes.nodes[milestoneIndex - 1];
+
showDropdown();
await waitForPromises();
- findDropdownItemAtIndex(milestoneIndex).vm.$emit('click');
+ findDropdown().vm.$emit('select', milestoneAtIndex.id);
hideDropdown();
await waitForPromises();
- expect(findDropdown().props('text')).toBe(milestoneAtIndex.title);
+ expect(findDropdown().props('toggleText')).toBe(milestoneAtIndex.title);
});
});
@@ -208,7 +199,7 @@ describe('WorkItemMilestone component', () => {
});
showDropdown();
- findFirstDropdownItem().vm.$emit('click');
+ findDropdown().vm.$emit('select', noMilestoneId);
hideDropdown();
await waitForPromises();
@@ -224,7 +215,7 @@ describe('WorkItemMilestone component', () => {
createComponent({ canUpdate: true });
showDropdown();
- findFirstDropdownItem().vm.$emit('click');
+ findDropdown().vm.$emit('select', noMilestoneId);
hideDropdown();
await waitForPromises();
diff --git a/spec/frontend/work_items/components/work_item_parent_spec.js b/spec/frontend/work_items/components/work_item_parent_spec.js
index a72eeabc43c..11fe6dffbfa 100644
--- a/spec/frontend/work_items/components/work_item_parent_spec.js
+++ b/spec/frontend/work_items/components/work_item_parent_spec.js
@@ -1,14 +1,15 @@
-import * as Sentry from '@sentry/browser';
import { GlCollapsibleListbox, GlFormGroup } from '@gitlab/ui';
-
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import waitForPromises from 'helpers/wait_for_promises';
import createMockApollo from 'helpers/mock_apollo_helper';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import WorkItemParent from '~/work_items/components/work_item_parent.vue';
+import { removeHierarchyChild } from '~/work_items/graphql/cache_utils';
import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql';
+import groupWorkItemsQuery from '~/work_items/graphql/group_work_items.query.graphql';
import projectWorkItemsQuery from '~/work_items/graphql/project_work_items.query.graphql';
import { WORK_ITEM_TYPE_ENUM_OBJECTIVE } from '~/work_items/constants';
@@ -20,7 +21,10 @@ import {
updateWorkItemMutationErrorResponse,
} from '../mock_data';
-jest.mock('@sentry/browser');
+jest.mock('~/sentry/sentry_browser_wrapper');
+jest.mock('~/work_items/graphql/cache_utils', () => ({
+ removeHierarchyChild: jest.fn(),
+}));
describe('WorkItemParent component', () => {
Vue.use(VueApollo);
@@ -29,7 +33,9 @@ describe('WorkItemParent component', () => {
const workItemId = 'gid://gitlab/WorkItem/1';
const workItemType = 'Objective';
+ const mockFullPath = 'full-path';
+ const groupWorkItemsSuccessHandler = jest.fn().mockResolvedValue(availableObjectivesResponse);
const availableWorkItemsSuccessHandler = jest.fn().mockResolvedValue(availableObjectivesResponse);
const availableWorkItemsFailureHandler = jest.fn().mockRejectedValue(new Error());
@@ -42,14 +48,17 @@ describe('WorkItemParent component', () => {
parent = null,
searchQueryHandler = availableWorkItemsSuccessHandler,
mutationHandler = successUpdateWorkItemMutationHandler,
+ isGroup = false,
} = {}) => {
wrapper = shallowMountExtended(WorkItemParent, {
apolloProvider: createMockApollo([
[projectWorkItemsQuery, searchQueryHandler],
+ [groupWorkItemsQuery, groupWorkItemsSuccessHandler],
[updateWorkItemMutation, mutationHandler],
]),
provide: {
- fullPath: 'full-path',
+ fullPath: mockFullPath,
+ isGroup,
},
propsData: {
canUpdate,
@@ -81,7 +90,6 @@ describe('WorkItemParent component', () => {
headerText: 'Assign parent',
category: 'tertiary',
loading: false,
- noCaret: true,
isCheckCentered: true,
searchable: true,
searching: false,
@@ -90,7 +98,6 @@ describe('WorkItemParent component', () => {
toggleText: 'None',
searchPlaceholder: 'Search',
resetButtonLabel: 'Unassign',
- block: true,
});
});
@@ -98,14 +105,12 @@ describe('WorkItemParent component', () => {
createComponent({ canUpdate: false, parent: mockParentWidgetResponse });
expect(findCollapsibleListbox().exists()).toBe(false);
- expect(findParentText().exists()).toBe(true);
expect(findParentText().text()).toBe('Objective 101');
});
it('shows loading while searching', async () => {
await findCollapsibleListbox().vm.$emit('shown');
expect(findCollapsibleListbox().props('searching')).toBe(true);
- expect(findCollapsibleListbox().props('no-caret')).toBeUndefined();
});
});
@@ -143,15 +148,27 @@ describe('WorkItemParent component', () => {
});
await findCollapsibleListbox().vm.$emit('shown');
- await findCollapsibleListbox().vm.$emit('search', 'Objective 101');
await waitForPromises();
expect(searchedItemQueryHandler).toHaveBeenCalledWith({
fullPath: 'full-path',
+ searchTerm: '',
+ types: [WORK_ITEM_TYPE_ENUM_OBJECTIVE],
+ in: undefined,
+ iid: null,
+ isNumber: false,
+ });
+
+ await findCollapsibleListbox().vm.$emit('search', 'Objective 101');
+
+ expect(searchedItemQueryHandler).toHaveBeenCalledWith({
+ fullPath: 'full-path',
searchTerm: 'Objective 101',
types: [WORK_ITEM_TYPE_ENUM_OBJECTIVE],
in: 'TITLE',
+ iid: null,
+ isNumber: false,
});
await nextTick();
@@ -164,7 +181,6 @@ describe('WorkItemParent component', () => {
describe('listbox', () => {
const selectWorkItem = async (workItem) => {
- await findCollapsibleListbox().vm.$emit('shown');
await findCollapsibleListbox().vm.$emit('select', workItem);
};
@@ -181,6 +197,14 @@ describe('WorkItemParent component', () => {
},
},
});
+
+ expect(removeHierarchyChild).toHaveBeenCalledWith({
+ cache: expect.anything(Object),
+ fullPath: mockFullPath,
+ iid: undefined,
+ isGroup: false,
+ workItem: { id: 'gid://gitlab/WorkItem/1' },
+ });
});
it('calls mutation when item is unassigned', async () => {
@@ -188,6 +212,9 @@ describe('WorkItemParent component', () => {
.fn()
.mockResolvedValue(updateWorkItemMutationResponseFactory({ parent: null }));
createComponent({
+ parent: {
+ iid: '1',
+ },
mutationHandler: unAssignParentWorkItemMutationHandler,
});
@@ -203,6 +230,13 @@ describe('WorkItemParent component', () => {
},
},
});
+ expect(removeHierarchyChild).toHaveBeenCalledWith({
+ cache: expect.anything(Object),
+ fullPath: mockFullPath,
+ iid: '1',
+ isGroup: false,
+ workItem: { id: 'gid://gitlab/WorkItem/1' },
+ });
});
it('emits error when mutation fails', async () => {
@@ -233,4 +267,34 @@ describe('WorkItemParent component', () => {
expect(Sentry.captureException).toHaveBeenCalledWith(error);
});
});
+
+ describe('when project context', () => {
+ beforeEach(() => {
+ createComponent();
+ findCollapsibleListbox().vm.$emit('shown');
+ });
+
+ it('calls the project work items query', () => {
+ expect(availableWorkItemsSuccessHandler).toHaveBeenCalled();
+ });
+
+ it('skips calling the group work items query', () => {
+ expect(groupWorkItemsSuccessHandler).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('when group context', () => {
+ beforeEach(() => {
+ createComponent({ isGroup: true });
+ findCollapsibleListbox().vm.$emit('shown');
+ });
+
+ it('skips calling the project work items query', () => {
+ expect(availableWorkItemsSuccessHandler).not.toHaveBeenCalled();
+ });
+
+ it('calls the group work items query', () => {
+ expect(groupWorkItemsSuccessHandler).toHaveBeenCalled();
+ });
+ });
});
diff --git a/spec/frontend/work_items/components/work_item_relationships/__snapshots__/work_item_relationship_list_spec.js.snap b/spec/frontend/work_items/components/work_item_relationships/__snapshots__/work_item_relationship_list_spec.js.snap
index bbc19a011a5..20af8584e37 100644
--- a/spec/frontend/work_items/components/work_item_relationships/__snapshots__/work_item_relationship_list_spec.js.snap
+++ b/spec/frontend/work_items/components/work_item_relationships/__snapshots__/work_item_relationship_list_spec.js.snap
@@ -22,6 +22,7 @@ exports[`WorkItemRelationshipList renders linked item list 1`] = `
<work-item-link-child-contents-stub
canupdate="true"
childitem="[object Object]"
+ showlabels="true"
showtaskicon="true"
/>
</li>
diff --git a/spec/frontend/work_items/components/work_item_relationships/work_item_add_relationship_form_spec.js b/spec/frontend/work_items/components/work_item_relationships/work_item_add_relationship_form_spec.js
index d7b3ced2ff9..520cf5f7ea4 100644
--- a/spec/frontend/work_items/components/work_item_relationships/work_item_add_relationship_form_spec.js
+++ b/spec/frontend/work_items/components/work_item_relationships/work_item_add_relationship_form_spec.js
@@ -34,6 +34,9 @@ describe('WorkItemAddRelationshipForm', () => {
wrapper = shallowMountExtended(WorkItemAddRelationshipForm, {
apolloProvider: mockApolloProvider,
+ provide: {
+ isGroup: false,
+ },
propsData: {
workItemId,
workItemIid,
diff --git a/spec/frontend/work_items/components/work_item_relationships/work_item_relationships_spec.js b/spec/frontend/work_items/components/work_item_relationships/work_item_relationships_spec.js
index 7178fa1aae7..0faea0e4862 100644
--- a/spec/frontend/work_items/components/work_item_relationships/work_item_relationships_spec.js
+++ b/spec/frontend/work_items/components/work_item_relationships/work_item_relationships_spec.js
@@ -1,6 +1,6 @@
-import Vue from 'vue';
+import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
-import { GlLoadingIcon } from '@gitlab/ui';
+import { GlLoadingIcon, GlToggle } from '@gitlab/ui';
import createMockApollo from 'helpers/mock_apollo_helper';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
@@ -82,6 +82,7 @@ describe('WorkItemRelationships', () => {
wrapper.findAllComponents(WorkItemRelationshipList);
const findAddButton = () => wrapper.findByTestId('link-item-add-button');
const findWorkItemRelationshipForm = () => wrapper.findComponent(WorkItemAddRelationshipForm);
+ const findShowLabelsToggle = () => wrapper.findComponent(GlToggle);
it('shows loading icon when query is not processed', () => {
createComponent();
@@ -99,6 +100,11 @@ describe('WorkItemRelationships', () => {
expect(findLinkedItemsHelpLink().attributes('href')).toBe(
'/help/user/okrs.md#linked-items-in-okrs',
);
+ expect(findShowLabelsToggle().props()).toMatchObject({
+ value: true,
+ labelPosition: 'left',
+ label: 'Show labels',
+ });
});
it('renders blocking linked item lists', async () => {
@@ -153,6 +159,29 @@ describe('WorkItemRelationships', () => {
expect(findWorkItemRelationshipForm().exists()).toBe(false);
});
+ it.each`
+ toggleValue
+ ${true}
+ ${false}
+ `(
+ 'passes showLabels as $toggleValue to child items when toggle is $toggleValue',
+ async ({ toggleValue }) => {
+ await createComponent({
+ workItemQueryHandler: jest
+ .fn()
+ .mockResolvedValue(workItemByIidResponseFactory({ linkedItems: mockLinkedItems })),
+ });
+
+ findShowLabelsToggle().vm.$emit('change', toggleValue);
+
+ await nextTick();
+
+ expect(findAllWorkItemRelationshipListComponents().at(0).props('showLabels')).toBe(
+ toggleValue,
+ );
+ },
+ );
+
describe('when project context', () => {
it('calls the project work item query', () => {
createComponent();
diff --git a/spec/frontend/work_items/components/work_item_state_toggle_button_spec.js b/spec/frontend/work_items/components/work_item_state_toggle_button_spec.js
index c0b206e5da4..a210bd50422 100644
--- a/spec/frontend/work_items/components/work_item_state_toggle_button_spec.js
+++ b/spec/frontend/work_items/components/work_item_state_toggle_button_spec.js
@@ -5,7 +5,7 @@ import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import { mockTracking } from 'helpers/tracking_helper';
import waitForPromises from 'helpers/wait_for_promises';
-import WorkItemStateToggleButton from '~/work_items/components/work_item_state_toggle_button.vue';
+import WorkItemStateToggle from '~/work_items/components/work_item_state_toggle.vue';
import {
STATE_OPEN,
STATE_CLOSED,
@@ -33,7 +33,7 @@ describe('Work Item State toggle button component', () => {
workItemState = STATE_OPEN,
workItemType = 'Task',
} = {}) => {
- wrapper = shallowMount(WorkItemStateToggleButton, {
+ wrapper = shallowMount(WorkItemStateToggle, {
apolloProvider: createMockApollo([[updateWorkItemMutation, mutationHandler]]),
propsData: {
workItemId: id,
diff --git a/spec/frontend/work_items/components/work_item_title_spec.js b/spec/frontend/work_items/components/work_item_title_spec.js
index 34391b74cf7..0f466bcf691 100644
--- a/spec/frontend/work_items/components/work_item_title_spec.js
+++ b/spec/frontend/work_items/components/work_item_title_spec.js
@@ -131,5 +131,25 @@ describe('WorkItemTitle component', () => {
property: 'type_Task',
});
});
+
+ describe('when title has more than 255 characters', () => {
+ const title = new Array(257).join('a');
+
+ it('does not call a mutation', () => {
+ createComponent();
+
+ findItemTitle().vm.$emit('title-changed', title);
+
+ expect(mutationSuccessHandler).not.toHaveBeenCalled();
+ });
+
+ it('emits an error message', () => {
+ createComponent();
+
+ findItemTitle().vm.$emit('title-changed', title);
+
+ expect(wrapper.emitted('error')).toEqual([['Title cannot have more than 255 characters.']]);
+ });
+ });
});
});
diff --git a/spec/frontend/work_items/components/work_item_todos_spec.js b/spec/frontend/work_items/components/work_item_todos_spec.js
index c76cdbcee53..d67d84e75b5 100644
--- a/spec/frontend/work_items/components/work_item_todos_spec.js
+++ b/spec/frontend/work_items/components/work_item_todos_spec.js
@@ -34,7 +34,7 @@ describe('WorkItemTodo component', () => {
const workItemQueryResponse = workItemResponseFactory({ canUpdate: true });
const mockWorkItemId = workItemQueryResponse.data.workItem.id;
const mockWorkItemIid = workItemQueryResponse.data.workItem.iid;
- const mockWorkItemFullpath = workItemQueryResponse.data.workItem.project.fullPath;
+ const mockWorkItemFullpath = workItemQueryResponse.data.workItem.namespace.fullPath;
const createTodoSuccessHandler = jest
.fn()
diff --git a/spec/frontend/work_items/list/components/work_items_list_app_spec.js b/spec/frontend/work_items/list/components/work_items_list_app_spec.js
index 96083478e77..401d7dcbbdb 100644
--- a/spec/frontend/work_items/list/components/work_items_list_app_spec.js
+++ b/spec/frontend/work_items/list/components/work_items_list_app_spec.js
@@ -1,7 +1,7 @@
-import * as Sentry from '@sentry/browser';
import { shallowMount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import IssueCardStatistics from 'ee_else_ce/issues/list/components/issue_card_statistics.vue';
import IssueCardTimeInfo from 'ee_else_ce/issues/list/components/issue_card_time_info.vue';
import createMockApollo from 'helpers/mock_apollo_helper';
@@ -12,7 +12,7 @@ import WorkItemsListApp from '~/work_items/list/components/work_items_list_app.v
import getWorkItemsQuery from '~/work_items/list/queries/get_work_items.query.graphql';
import { groupWorkItemsQueryResponse } from '../../mock_data';
-jest.mock('@sentry/browser');
+jest.mock('~/sentry/sentry_browser_wrapper');
describe('WorkItemsListApp component', () => {
let wrapper;
diff --git a/spec/frontend/work_items/mock_data.js b/spec/frontend/work_items/mock_data.js
index 9eb604c81cb..41e8a01de36 100644
--- a/spec/frontend/work_items/mock_data.js
+++ b/spec/frontend/work_items/mock_data.js
@@ -112,6 +112,7 @@ export const workItemQueryResponse = {
__typename: 'WorkItem',
id: 'gid://gitlab/WorkItem/1',
iid: '1',
+ archived: false,
title: 'Test',
state: 'OPEN',
description: 'description',
@@ -127,11 +128,10 @@ export const workItemQueryResponse = {
webUrl: 'http://127.0.0.1:3000/root',
__typename: 'UserCore',
},
- project: {
+ namespace: {
__typename: 'Project',
id: '1',
fullPath: 'test-project-path',
- archived: false,
name: 'Project name',
},
workItemType: {
@@ -224,6 +224,7 @@ export const updateWorkItemMutationResponse = {
__typename: 'WorkItem',
id: 'gid://gitlab/WorkItem/1',
iid: '1',
+ archived: false,
title: 'Updated title',
state: 'OPEN',
description: 'description',
@@ -234,11 +235,10 @@ export const updateWorkItemMutationResponse = {
author: {
...mockAssignees[0],
},
- project: {
+ namespace: {
__typename: 'Project',
id: '1',
fullPath: 'test-project-path',
- archived: false,
name: 'Project name',
},
workItemType: {
@@ -335,6 +335,7 @@ export const convertWorkItemMutationResponse = {
__typename: 'WorkItem',
id: 'gid://gitlab/WorkItem/1',
iid: '1',
+ archived: false,
title: 'Updated title',
state: 'OPEN',
description: 'description',
@@ -345,11 +346,10 @@ export const convertWorkItemMutationResponse = {
author: {
...mockAssignees[0],
},
- project: {
+ namespace: {
__typename: 'Project',
id: '1',
fullPath: 'test-project-path',
- archived: false,
name: 'Project name',
},
workItemType: {
@@ -626,6 +626,7 @@ export const workItemResponseFactory = ({
__typename: 'WorkItem',
id: 'gid://gitlab/WorkItem/1',
iid,
+ archived: false,
title: 'Updated title',
state,
description: 'description',
@@ -634,11 +635,10 @@ export const workItemResponseFactory = ({
updatedAt,
closedAt: null,
author,
- project: {
+ namespace: {
__typename: 'Project',
id: '1',
fullPath: 'test-project-path',
- archived: false,
name: 'Project name',
},
workItemType,
@@ -901,6 +901,7 @@ export const createWorkItemMutationResponse = {
__typename: 'WorkItem',
id: 'gid://gitlab/WorkItem/1',
iid: '1',
+ archived: false,
title: 'Updated title',
state: 'OPEN',
description: 'description',
@@ -911,11 +912,10 @@ export const createWorkItemMutationResponse = {
author: {
...mockAssignees[0],
},
- project: {
+ namespace: {
__typename: 'Project',
id: '1',
fullPath: 'test-project-path',
- archived: false,
name: 'Project name',
},
workItemType: {
@@ -987,6 +987,7 @@ export const workItemHierarchyEmptyResponse = {
{
id: 'gid://gitlab/WorkItem/1',
iid: '1',
+ archived: false,
state: 'OPEN',
workItemType: {
id: 'gid://gitlab/WorkItems::Type/1',
@@ -1000,11 +1001,10 @@ export const workItemHierarchyEmptyResponse = {
updatedAt: null,
closedAt: null,
author: mockAssignees[0],
- project: {
+ namespace: {
__typename: 'Project',
id: '1',
fullPath: 'test-project-path',
- archived: false,
name: 'Project name',
},
userPermissions: {
@@ -1045,6 +1045,7 @@ export const workItemHierarchyNoUpdatePermissionResponse = {
workItem: {
id: 'gid://gitlab/WorkItem/1',
iid: '1',
+ archived: false,
state: 'OPEN',
workItemType: {
id: 'gid://gitlab/WorkItems::Type/6',
@@ -1067,11 +1068,10 @@ export const workItemHierarchyNoUpdatePermissionResponse = {
adminWorkItemLink: true,
__typename: 'WorkItemPermissions',
},
- project: {
+ namespace: {
__typename: 'Project',
id: '1',
fullPath: 'test-project-path',
- archived: false,
name: 'Project name',
},
confidential: false,
@@ -1207,6 +1207,7 @@ export const workItemHierarchyResponse = {
{
id: 'gid://gitlab/WorkItem/1',
iid: '1',
+ archived: false,
workItemType: {
id: 'gid://gitlab/WorkItems::Type/1',
name: 'Issue',
@@ -1227,11 +1228,10 @@ export const workItemHierarchyResponse = {
...mockAssignees[0],
},
confidential: false,
- project: {
+ namespace: {
__typename: 'Project',
id: '1',
fullPath: 'test-project-path',
- archived: false,
name: 'Project name',
},
description: 'Issue description',
@@ -1303,17 +1303,17 @@ export const workItemObjectiveMetadataWidgets = {
export const workItemObjectiveWithChild = {
id: 'gid://gitlab/WorkItem/12',
iid: '12',
+ archived: false,
workItemType: {
id: 'gid://gitlab/WorkItems::Type/2411',
name: 'Objective',
iconName: 'issue-type-objective',
__typename: 'WorkItemType',
},
- project: {
+ namespace: {
__typename: 'Project',
id: '1',
fullPath: 'test-project-path',
- archived: false,
name: 'Project name',
},
userPermissions: {
@@ -1381,6 +1381,7 @@ export const workItemHierarchyTreeResponse = {
workItem: {
id: 'gid://gitlab/WorkItem/2',
iid: '2',
+ archived: false,
workItemType: {
id: 'gid://gitlab/WorkItems::Type/2411',
name: 'Objective',
@@ -1398,11 +1399,10 @@ export const workItemHierarchyTreeResponse = {
__typename: 'WorkItemPermissions',
},
confidential: false,
- project: {
+ namespace: {
__typename: 'Project',
id: '1',
fullPath: 'test-project-path',
- archived: false,
name: 'Project name',
},
widgets: [
@@ -1483,6 +1483,7 @@ export const changeIndirectWorkItemParentMutationResponse = {
description: null,
id: 'gid://gitlab/WorkItem/13',
iid: '13',
+ archived: false,
state: 'OPEN',
title: 'Objective 2',
confidential: false,
@@ -1492,11 +1493,10 @@ export const changeIndirectWorkItemParentMutationResponse = {
author: {
...mockAssignees[0],
},
- project: {
+ namespace: {
__typename: 'Project',
id: '1',
fullPath: 'test-project-path',
- archived: false,
name: 'Project name',
},
reference: 'test-project-path#13',
@@ -1552,6 +1552,7 @@ export const changeWorkItemParentMutationResponse = {
description: null,
id: 'gid://gitlab/WorkItem/2',
iid: '2',
+ archived: false,
state: 'OPEN',
title: 'Foo',
confidential: false,
@@ -1561,11 +1562,10 @@ export const changeWorkItemParentMutationResponse = {
author: {
...mockAssignees[0],
},
- project: {
+ namespace: {
__typename: 'Project',
id: '1',
fullPath: 'test-project-path',
- archived: false,
name: 'Project name',
},
reference: 'test-project-path#2',
@@ -1600,27 +1600,18 @@ export const availableWorkItemsResponse = {
id: 'gid://gitlab/WorkItem/458',
iid: '2',
title: 'Task 1',
- state: 'OPEN',
- createdAt: '2022-08-03T12:41:54Z',
- confidential: false,
__typename: 'WorkItem',
},
{
id: 'gid://gitlab/WorkItem/459',
iid: '3',
title: 'Task 2',
- state: 'OPEN',
- createdAt: '2022-08-03T12:41:54Z',
- confidential: false,
__typename: 'WorkItem',
},
{
id: 'gid://gitlab/WorkItem/460',
iid: '4',
title: 'Task 3',
- state: 'OPEN',
- createdAt: '2022-08-03T12:41:54Z',
- confidential: true,
__typename: 'WorkItem',
},
],
@@ -1640,24 +1631,18 @@ export const availableObjectivesResponse = {
id: 'gid://gitlab/WorkItem/716',
iid: '122',
title: 'Objective 101',
- state: 'OPEN',
- confidential: false,
__typename: 'WorkItem',
},
{
id: 'gid://gitlab/WorkItem/712',
iid: '118',
title: 'Objective 103',
- state: 'OPEN',
- confidential: false,
__typename: 'WorkItem',
},
{
id: 'gid://gitlab/WorkItem/711',
iid: '117',
title: 'Objective 102',
- state: 'OPEN',
- confidential: false,
__typename: 'WorkItem',
},
],
@@ -1677,8 +1662,6 @@ export const searchedObjectiveResponse = {
id: 'gid://gitlab/WorkItem/716',
iid: '122',
title: 'Objective 101',
- state: 'OPEN',
- confidential: false,
__typename: 'WorkItem',
},
],
@@ -1687,7 +1670,7 @@ export const searchedObjectiveResponse = {
},
};
-export const searchedWorkItemsResponse = {
+export const searchWorkItemsTextResponse = {
data: {
workspace: {
__typename: 'Project',
@@ -1698,9 +1681,57 @@ export const searchedWorkItemsResponse = {
id: 'gid://gitlab/WorkItem/459',
iid: '3',
title: 'Task 2',
- state: 'OPEN',
- createdAt: '2022-08-03T12:41:54Z',
- confidential: false,
+ __typename: 'WorkItem',
+ },
+ ],
+ },
+ },
+ },
+};
+
+export const searchWorkItemsIidResponse = {
+ data: {
+ workspace: {
+ __typename: 'Project',
+ id: 'gid://gitlab/Project/2',
+ workItems: {
+ nodes: [],
+ },
+ workItemsByIid: {
+ nodes: [
+ {
+ id: 'gid://gitlab/WorkItem/460',
+ iid: '101',
+ title: 'Task 3',
+ __typename: 'WorkItem',
+ },
+ ],
+ },
+ },
+ },
+};
+
+export const searchWorkItemsTextIidResponse = {
+ data: {
+ workspace: {
+ __typename: 'Project',
+ id: 'gid://gitlab/Project/2',
+ workItems: {
+ nodes: [
+ {
+ id: 'gid://gitlab/WorkItem/459',
+ iid: '3',
+ title: 'Task 123',
+ __typename: 'WorkItem',
+ },
+ ],
+ },
+ workItemsByIid: {
+ nodes: [
+ {
+ id: 'gid://gitlab/WorkItem/460',
+ iid: '123',
+ title: 'Task 2',
__typename: 'WorkItem',
},
],
diff --git a/spec/frontend_integration/snippets/snippets_notes_spec.js b/spec/frontend_integration/snippets/snippets_notes_spec.js
index 27be7793ce6..b7d2c8924f6 100644
--- a/spec/frontend_integration/snippets/snippets_notes_spec.js
+++ b/spec/frontend_integration/snippets/snippets_notes_spec.js
@@ -51,7 +51,7 @@ describe('Integration Snippets notes', () => {
'circled latin capital letter m',
],
],
- [':', ['100', '1234', '8ball', 'a', 'ab']],
+ [':', ['grinning', 'smiley', 'smile', 'grin', 'laughing']],
// We do not want the search to start with space https://gitlab.com/gitlab-org/gitlab/-/issues/322548
[': ', []],
// We want to preserve that we can have space INSIDE the search
diff --git a/spec/frontend_integration/test_helpers/mock_server/routes/emojis.js b/spec/frontend_integration/test_helpers/mock_server/routes/emojis.js
index 83991ad5af9..72953b94552 100644
--- a/spec/frontend_integration/test_helpers/mock_server/routes/emojis.js
+++ b/spec/frontend_integration/test_helpers/mock_server/routes/emojis.js
@@ -1,5 +1,5 @@
import { Response } from 'miragejs';
-import emojis from 'public/-/emojis/2/emojis.json';
+import emojis from 'public/-/emojis/3/emojis.json';
import { EMOJI_VERSION } from '~/emoji';
import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
diff --git a/spec/frontend_integration/test_helpers/mock_server/routes/graphql.js b/spec/frontend_integration/test_helpers/mock_server/routes/graphql.js
index a22763dcb45..d9b8f8aaeb5 100644
--- a/spec/frontend_integration/test_helpers/mock_server/routes/graphql.js
+++ b/spec/frontend_integration/test_helpers/mock_server/routes/graphql.js
@@ -2,10 +2,8 @@ import { graphqlQuery } from '../graphql';
export default (server) => {
server.post('/api/graphql', (schema, request) => {
- const batches = JSON.parse(request.requestBody);
+ const { query, variables } = JSON.parse(request.requestBody);
- return Promise.all(
- batches.map(({ query, variables }) => graphqlQuery(query, variables, schema)),
- );
+ return graphqlQuery(query, variables, schema);
});
};
diff --git a/spec/graphql/mutations/base_mutation_spec.rb b/spec/graphql/mutations/base_mutation_spec.rb
deleted file mode 100644
index a73d914f48f..00000000000
--- a/spec/graphql/mutations/base_mutation_spec.rb
+++ /dev/null
@@ -1,58 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe ::Mutations::BaseMutation, feature_category: :api do
- include GraphqlHelpers
-
- describe 'argument nullability' do
- let_it_be(:user) { create(:user) }
- let_it_be(:context) { { current_user: user } }
-
- subject(:mutation) { mutation_class.new(object: nil, context: context, field: nil) }
-
- describe 'when using a mutation with correct argument declarations' do
- context 'when argument is nullable and required' do
- let(:mutation_class) do
- Class.new(described_class) do
- graphql_name 'BaseMutation'
- argument :foo, GraphQL::Types::String, required: :nullable
- end
- end
-
- specify do
- expect { subject.ready? }.to raise_error(ArgumentError, /must be provided: foo/)
- end
-
- specify do
- expect { subject.ready?(foo: nil) }.not_to raise_error
- end
-
- specify do
- expect { subject.ready?(foo: "bar") }.not_to raise_error
- end
- end
-
- context 'when argument is required and NOT nullable' do
- let(:mutation_class) do
- Class.new(described_class) do
- graphql_name 'BaseMutation'
- argument :foo, GraphQL::Types::String, required: true
- end
- end
-
- specify do
- expect { subject.ready? }.to raise_error(ArgumentError, /must be provided/)
- end
-
- specify do
- expect { subject.ready?(foo: nil) }.to raise_error(ArgumentError, /must be provided/)
- end
-
- specify do
- expect { subject.ready?(foo: "bar") }.not_to raise_error
- end
- end
- end
- end
-end
diff --git a/spec/graphql/mutations/ci/runner/delete_spec.rb b/spec/graphql/mutations/ci/runner/delete_spec.rb
index f19fa7c34a9..beff18e1dfd 100644
--- a/spec/graphql/mutations/ci/runner/delete_spec.rb
+++ b/spec/graphql/mutations/ci/runner/delete_spec.rb
@@ -48,7 +48,7 @@ RSpec.describe Mutations::Ci::Runner::Delete, feature_category: :runner_fleet do
let(:mutation_params) { {} }
it 'raises an error' do
- expect { subject }.to raise_error(ArgumentError, "Arguments must be provided: id")
+ expect { subject }.to raise_error(ArgumentError, "missing keyword: :id")
end
end
diff --git a/spec/graphql/mutations/ci/runner/update_spec.rb b/spec/graphql/mutations/ci/runner/update_spec.rb
index 02bb7ee2170..03bfd4d738b 100644
--- a/spec/graphql/mutations/ci/runner/update_spec.rb
+++ b/spec/graphql/mutations/ci/runner/update_spec.rb
@@ -42,7 +42,7 @@ RSpec.describe Mutations::Ci::Runner::Update, feature_category: :runner_fleet do
let(:mutation_params) { {} }
it 'raises an error' do
- expect { response }.to raise_error(ArgumentError, "Arguments must be provided: id")
+ expect { response }.to raise_error(ArgumentError, "missing keyword: :id")
end
end
diff --git a/spec/graphql/mutations/container_repositories/destroy_tags_spec.rb b/spec/graphql/mutations/container_repositories/destroy_tags_spec.rb
index 96dd1754155..9a06eb81903 100644
--- a/spec/graphql/mutations/container_repositories/destroy_tags_spec.rb
+++ b/spec/graphql/mutations/container_repositories/destroy_tags_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Mutations::ContainerRepositories::DestroyTags do
+RSpec.describe Mutations::ContainerRepositories::DestroyTags, feature_category: :container_registry do
include GraphqlHelpers
include_context 'container repository delete tags service shared context'
@@ -23,7 +23,7 @@ RSpec.describe Mutations::ContainerRepositories::DestroyTags do
shared_examples 'destroying container repository tags' do
before do
stub_delete_reference_requests(tags)
- expect_delete_tag_by_names(tags)
+ expect_delete_tags(tags)
allow_next_instance_of(ContainerRegistry::Client) do |client|
allow(client).to receive(:supports_tag_delete?).and_return(true)
end
diff --git a/spec/graphql/mutations/merge_requests/update_spec.rb b/spec/graphql/mutations/merge_requests/update_spec.rb
index 6ced71c5f4c..c34ec939d12 100644
--- a/spec/graphql/mutations/merge_requests/update_spec.rb
+++ b/spec/graphql/mutations/merge_requests/update_spec.rb
@@ -153,20 +153,6 @@ RSpec.describe Mutations::MergeRequests::Update, feature_category: :team_plannin
subject(:ready) { mutation.ready?(**arguments) }
- context 'when required arguments are not provided' do
- let(:arguments) { {} }
-
- it 'raises an argument error' do
- expect { subject }.to raise_error(ArgumentError, 'Arguments must be provided: projectPath, iid')
- end
- end
-
- context 'when required arguments are provided' do
- it 'returns true' do
- expect(subject).to eq(true)
- end
- end
-
context 'when timeEstimate is provided' do
let(:extra_args) { { time_estimate: time_estimate } }
diff --git a/spec/graphql/mutations/namespace/package_settings/update_spec.rb b/spec/graphql/mutations/namespace/package_settings/update_spec.rb
index b7f9eac3755..f4e79481d44 100644
--- a/spec/graphql/mutations/namespace/package_settings/update_spec.rb
+++ b/spec/graphql/mutations/namespace/package_settings/update_spec.rb
@@ -73,18 +73,6 @@ RSpec.describe Mutations::Namespace::PackageSettings::Update, feature_category:
)
end
end
-
- context 'when nuget_duplicates_option FF is disabled' do
- let_it_be(:params) { { namespace_path: namespace.full_path, nuget_duplicates_allowed: false } }
-
- before do
- stub_feature_flags(nuget_duplicates_option: false)
- end
-
- it 'raises an error' do
- expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable, /feature flag is disabled/)
- end
- end
end
RSpec.shared_examples 'denying access to namespace package setting' do
diff --git a/spec/graphql/mutations/saved_replies/create_spec.rb b/spec/graphql/mutations/saved_replies/create_spec.rb
index 9423ba2b354..9db23ab5345 100644
--- a/spec/graphql/mutations/saved_replies/create_spec.rb
+++ b/spec/graphql/mutations/saved_replies/create_spec.rb
@@ -14,33 +14,17 @@ RSpec.describe Mutations::SavedReplies::Create do
mutation.resolve(**mutation_arguments)
end
- context 'when feature is disabled' do
- before do
- stub_feature_flags(saved_replies: false)
- end
-
- it 'raises Gitlab::Graphql::Errors::ResourceNotAvailable' do
- expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable, 'Feature disabled')
- end
- end
-
- context 'when feature is enabled for current user' do
- before do
- stub_feature_flags(saved_replies: current_user)
- end
+ context 'when service fails to create a new saved reply' do
+ let(:mutation_arguments) { { name: '', content: '' } }
- context 'when service fails to create a new saved reply' do
- let(:mutation_arguments) { { name: '', content: '' } }
-
- it { expect(subject[:saved_reply]).to be_nil }
- it { expect(subject[:errors]).to match_array(["Content can't be blank", "Name can't be blank"]) }
- end
+ it { expect(subject[:saved_reply]).to be_nil }
+ it { expect(subject[:errors]).to match_array(["Content can't be blank", "Name can't be blank"]) }
+ end
- context 'when service successfully creates a new saved reply' do
- it { expect(subject[:saved_reply].name).to eq(mutation_arguments[:name]) }
- it { expect(subject[:saved_reply].content).to eq(mutation_arguments[:content]) }
- it { expect(subject[:errors]).to be_empty }
- end
+ context 'when service successfully creates a new saved reply' do
+ it { expect(subject[:saved_reply].name).to eq(mutation_arguments[:name]) }
+ it { expect(subject[:saved_reply].content).to eq(mutation_arguments[:content]) }
+ it { expect(subject[:errors]).to be_empty }
end
end
end
diff --git a/spec/graphql/mutations/saved_replies/destroy_spec.rb b/spec/graphql/mutations/saved_replies/destroy_spec.rb
index 6cff28ec0b2..84efd9ee0b8 100644
--- a/spec/graphql/mutations/saved_replies/destroy_spec.rb
+++ b/spec/graphql/mutations/saved_replies/destroy_spec.rb
@@ -13,34 +13,18 @@ RSpec.describe Mutations::SavedReplies::Destroy do
mutation.resolve(id: saved_reply.to_global_id)
end
- context 'when feature is disabled' do
+ context 'when service fails to delete a new saved reply' do
before do
- stub_feature_flags(saved_replies: false)
+ saved_reply.destroy!
end
it 'raises Gitlab::Graphql::Errors::ResourceNotAvailable' do
- expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable, 'Feature disabled')
+ expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
end
end
- context 'when feature is enabled for current user' do
- before do
- stub_feature_flags(saved_replies: current_user)
- end
-
- context 'when service fails to delete a new saved reply' do
- before do
- saved_reply.destroy!
- end
-
- it 'raises Gitlab::Graphql::Errors::ResourceNotAvailable' do
- expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
- end
- end
-
- context 'when service successfully deletes the saved reply' do
- it { expect(subject[:errors]).to be_empty }
- end
+ context 'when service successfully deletes the saved reply' do
+ it { expect(subject[:errors]).to be_empty }
end
end
end
diff --git a/spec/graphql/mutations/saved_replies/update_spec.rb b/spec/graphql/mutations/saved_replies/update_spec.rb
index 9b0e90b7b41..cc358d946a5 100644
--- a/spec/graphql/mutations/saved_replies/update_spec.rb
+++ b/spec/graphql/mutations/saved_replies/update_spec.rb
@@ -15,33 +15,17 @@ RSpec.describe Mutations::SavedReplies::Update do
mutation.resolve(id: saved_reply.to_global_id, **mutation_arguments)
end
- context 'when feature is disabled' do
- before do
- stub_feature_flags(saved_replies: false)
- end
-
- it 'raises Gitlab::Graphql::Errors::ResourceNotAvailable' do
- expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable, 'Feature disabled')
- end
- end
-
- context 'when feature is enabled for current user' do
- before do
- stub_feature_flags(saved_replies: current_user)
- end
+ context 'when service fails to update a new saved reply' do
+ let(:mutation_arguments) { { name: '', content: '' } }
- context 'when service fails to update a new saved reply' do
- let(:mutation_arguments) { { name: '', content: '' } }
-
- it { expect(subject[:saved_reply]).to be_nil }
- it { expect(subject[:errors]).to match_array(["Content can't be blank", "Name can't be blank"]) }
- end
+ it { expect(subject[:saved_reply]).to be_nil }
+ it { expect(subject[:errors]).to match_array(["Content can't be blank", "Name can't be blank"]) }
+ end
- context 'when service successfully updates the saved reply' do
- it { expect(subject[:saved_reply].name).to eq(mutation_arguments[:name]) }
- it { expect(subject[:saved_reply].content).to eq(mutation_arguments[:content]) }
- it { expect(subject[:errors]).to be_empty }
- end
+ context 'when service successfully updates the saved reply' do
+ it { expect(subject[:saved_reply].name).to eq(mutation_arguments[:name]) }
+ it { expect(subject[:saved_reply].content).to eq(mutation_arguments[:content]) }
+ it { expect(subject[:errors]).to be_empty }
end
end
end
diff --git a/spec/graphql/resolvers/ci/catalog/resource_resolver_spec.rb b/spec/graphql/resolvers/ci/catalog/resource_resolver_spec.rb
new file mode 100644
index 00000000000..19fc0c7fc4c
--- /dev/null
+++ b/spec/graphql/resolvers/ci/catalog/resource_resolver_spec.rb
@@ -0,0 +1,123 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Resolvers::Ci::Catalog::ResourceResolver, feature_category: :pipeline_composition do
+ include GraphqlHelpers
+
+ let_it_be(:namespace) { create(:group) }
+ let_it_be(:project) { create(:project, :private, namespace: namespace) }
+ let_it_be(:resource) { create(:ci_catalog_resource, project: project) }
+ let_it_be(:user) { create(:user) }
+
+ describe '#resolve' do
+ context 'when id argument is provided' do
+ context 'when the user is authorised to view the resource' do
+ before_all do
+ namespace.add_developer(user)
+ end
+
+ context 'when resource is found' do
+ it 'returns a single CI/CD Catalog resource' do
+ result = resolve(described_class, ctx: { current_user: user },
+ args: { id: resource.to_global_id.to_s })
+
+ expect(result.id).to eq(resource.id)
+ expect(result.class).to eq(Ci::Catalog::Resource)
+ end
+ end
+
+ context 'when resource is not found' do
+ it 'raises ResourceNotAvailable error' do
+ result = resolve(described_class, ctx: { current_user: user },
+ args: { id: "gid://gitlab/Ci::Catalog::Resource/not-a-real-id" })
+
+ expect(result).to be_a(::Gitlab::Graphql::Errors::ResourceNotAvailable)
+ end
+ end
+ end
+
+ context 'when user is not authorised to view the resource' do
+ it 'raises ResourceNotAvailable error' do
+ result = resolve(described_class, ctx: { current_user: user },
+ args: { id: resource.to_global_id.to_s })
+
+ expect(result).to be_a(::Gitlab::Graphql::Errors::ResourceNotAvailable)
+ end
+ end
+ end
+
+ context 'when full_path argument is provided' do
+ context 'when the user is authorised to view the resource' do
+ before_all do
+ namespace.add_developer(user)
+ end
+
+ context 'when resource is found' do
+ it 'returns a single CI/CD Catalog resource' do
+ result = resolve(described_class, ctx: { current_user: user },
+ args: { full_path: resource.project.full_path })
+
+ expect(result.id).to eq(resource.id)
+ expect(result.class).to eq(Ci::Catalog::Resource)
+ end
+ end
+
+ context 'when resource is not found' do
+ it 'raises ResourceNotAvailable error' do
+ result = resolve(described_class, ctx: { current_user: user },
+ args: { full_path: "project/non_exisitng_resource" })
+
+ expect(result).to be_a(::Gitlab::Graphql::Errors::ResourceNotAvailable)
+ end
+ end
+
+ context 'when project is not a catalog resource' do
+ let_it_be(:project) { create(:project, :private, namespace: namespace) }
+
+ it 'raises ResourceNotAvailable error' do
+ result = resolve(described_class, ctx: { current_user: user }, args: { full_path: project.full_path })
+
+ expect(result).to be_a(::Gitlab::Graphql::Errors::ResourceNotAvailable)
+ end
+ end
+ end
+
+ context 'when user is not authorised to view the resource' do
+ it 'raises ResourceNotAvailable error' do
+ result = resolve(described_class, ctx: { current_user: user },
+ args: { full_path: resource.project.full_path })
+
+ expect(result).to be_a(::Gitlab::Graphql::Errors::ResourceNotAvailable)
+ end
+ end
+ end
+
+ context 'when neither id nor full_path argument is provided' do
+ before_all do
+ namespace.add_developer(user)
+ end
+ it 'raises ArgumentError' do
+ expect_graphql_error_to_be_created(::Gitlab::Graphql::Errors::ArgumentError,
+ "Exactly one of 'id' or 'full_path' arguments is required.") do
+ resolve(described_class, ctx: { current_user: user },
+ args: {})
+ end
+ end
+ end
+
+ context 'when both full_path and id arguments are provided' do
+ before_all do
+ namespace.add_developer(user)
+ end
+
+ it 'raises ArgumentError' do
+ expect_graphql_error_to_be_created(::Gitlab::Graphql::Errors::ArgumentError,
+ "Exactly one of 'id' or 'full_path' arguments is required.") do
+ resolve(described_class, ctx: { current_user: user },
+ args: { full_path: resource.project.full_path, id: resource.to_global_id.to_s })
+ end
+ end
+ end
+ end
+end
diff --git a/spec/graphql/resolvers/ci/catalog/resources_resolver_spec.rb b/spec/graphql/resolvers/ci/catalog/resources_resolver_spec.rb
new file mode 100644
index 00000000000..97105db686f
--- /dev/null
+++ b/spec/graphql/resolvers/ci/catalog/resources_resolver_spec.rb
@@ -0,0 +1,71 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Resolvers::Ci::Catalog::ResourcesResolver, feature_category: :pipeline_composition do
+ include GraphqlHelpers
+
+ let_it_be(:namespace) { create(:group) }
+ let_it_be(:project_1) { create(:project, name: 'Z', namespace: namespace) }
+ let_it_be(:project_2) { create(:project, name: 'A_Test', namespace: namespace) }
+ let_it_be(:project_3) { create(:project, name: 'L', description: 'Test', namespace: namespace) }
+ let_it_be(:resource_1) { create(:ci_catalog_resource, project: project_1) }
+ let_it_be(:resource_2) { create(:ci_catalog_resource, project: project_2) }
+ let_it_be(:resource_3) { create(:ci_catalog_resource, project: project_3) }
+ let_it_be(:user) { create(:user) }
+
+ let(:ctx) { { current_user: user } }
+ let(:search) { nil }
+ let(:sort) { nil }
+
+ let(:args) do
+ {
+ project_path: project_1.full_path,
+ sort: sort,
+ search: search
+ }.compact
+ end
+
+ subject(:result) { resolve(described_class, ctx: ctx, args: args) }
+
+ describe '#resolve' do
+ context 'with an authorized user' do
+ before_all do
+ namespace.add_owner(user)
+ end
+
+ it 'returns all catalog resources visible to the current user in the namespace' do
+ expect(result.items.count).to be(3)
+ expect(result.items.pluck(:name)).to contain_exactly('Z', 'A_Test', 'L')
+ end
+
+ context 'when the sort parameter is not provided' do
+ it 'returns all catalog resources sorted by descending created date' do
+ expect(result.items.pluck(:name)).to eq(%w[L A_Test Z])
+ end
+ end
+
+ context 'when the sort parameter is provided' do
+ let(:sort) { 'NAME_DESC' }
+
+ it 'returns all catalog resources sorted by descending name' do
+ expect(result.items.pluck(:name)).to eq(%w[Z L A_Test])
+ end
+ end
+
+ context 'when the search parameter is provided' do
+ let(:search) { 'test' }
+
+ it 'returns the catalog resources that match the search term' do
+ expect(result.items.pluck(:name)).to contain_exactly('A_Test', 'L')
+ end
+ end
+ end
+
+ context 'when the current user cannot read the namespace catalog' do
+ it 'returns empty response' do
+ expect(result).to be_empty
+ end
+ end
+ end
+end
diff --git a/spec/graphql/resolvers/ci/catalog/versions_resolver_spec.rb b/spec/graphql/resolvers/ci/catalog/versions_resolver_spec.rb
new file mode 100644
index 00000000000..02fb3dfaee4
--- /dev/null
+++ b/spec/graphql/resolvers/ci/catalog/versions_resolver_spec.rb
@@ -0,0 +1,66 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+# In this context, a `version` is equivalent to a `release`
+RSpec.describe Resolvers::Ci::Catalog::VersionsResolver, feature_category: :pipeline_composition do
+ include GraphqlHelpers
+
+ let_it_be(:today) { Time.now }
+ let_it_be(:yesterday) { today - 1.day }
+ let_it_be(:tomorrow) { today + 1.day }
+
+ let_it_be(:project) { create(:project, :private) }
+ # rubocop: disable Layout/LineLength
+ let_it_be(:version1) { create(:release, project: project, tag: 'v1.0.0', released_at: yesterday, created_at: tomorrow) }
+ let_it_be(:version2) { create(:release, project: project, tag: 'v2.0.0', released_at: today, created_at: yesterday) }
+ let_it_be(:version3) { create(:release, project: project, tag: 'v3.0.0', released_at: tomorrow, created_at: today) }
+ # rubocop: enable Layout/LineLength
+ let_it_be(:developer) { create(:user) }
+ let_it_be(:public_user) { create(:user) }
+
+ let(:args) { { sort: :released_at_desc } }
+ let(:all_releases) { [version1, version2, version3] }
+
+ before_all do
+ project.add_developer(developer)
+ end
+
+ describe '#resolve' do
+ it_behaves_like 'releases and group releases resolver'
+
+ describe 'when order_by is created_at' do
+ let(:current_user) { developer }
+
+ context 'with sort: desc' do
+ let(:args) { { sort: :created_desc } }
+
+ it 'returns the releases ordered by created_at in descending order' do
+ expect(resolve_releases.to_a)
+ .to match_array(all_releases)
+ .and be_sorted(:created_at, :desc)
+ end
+ end
+
+ context 'with sort: asc' do
+ let(:args) { { sort: :created_asc } }
+
+ it 'returns the releases ordered by created_at in ascending order' do
+ expect(resolve_releases.to_a)
+ .to match_array(all_releases)
+ .and be_sorted(:created_at, :asc)
+ end
+ end
+ end
+ end
+
+ private
+
+ def resolve_versions
+ context = { current_user: current_user }
+ resolve(described_class, obj: project, args: args, ctx: context, arg_style: :internal)
+ end
+
+ # Required for shared examples
+ alias_method :resolve_releases, :resolve_versions
+end
diff --git a/spec/graphql/resolvers/ci/runners_resolver_spec.rb b/spec/graphql/resolvers/ci/runners_resolver_spec.rb
index c164393d605..7d37d13366c 100644
--- a/spec/graphql/resolvers/ci/runners_resolver_spec.rb
+++ b/spec/graphql/resolvers/ci/runners_resolver_spec.rb
@@ -85,7 +85,9 @@ RSpec.describe Resolvers::Ci::RunnersResolver, feature_category: :runner_fleet d
type: :instance_type,
tag_list: ['active_runner'],
search: 'abc',
- sort: :contacted_asc
+ sort: :contacted_asc,
+ creator_id: 'gid://gitlab/User/1',
+ version_prefix: '15.'
}
end
@@ -98,7 +100,9 @@ RSpec.describe Resolvers::Ci::RunnersResolver, feature_category: :runner_fleet d
tag_name: ['active_runner'],
preload: false,
search: 'abc',
- sort: 'contacted_asc'
+ sort: 'contacted_asc',
+ creator_id: '1',
+ version_prefix: '15.'
}
end
@@ -169,6 +173,26 @@ RSpec.describe Resolvers::Ci::RunnersResolver, feature_category: :runner_fleet d
expect(resolve_scope.items.to_a).to contain_exactly :execute_return_value
end
end
+
+ context 'with an invalid version filter parameter' do
+ let(:args) do
+ { version_prefix: 'a.b' }
+ end
+
+ let(:expected_params) do
+ {
+ preload: false,
+ version_prefix: 'a.b'
+ }
+ end
+
+ it 'ignores the parameter and returns runners' do
+ expect(::Ci::RunnersFinder).to receive(:new).with(current_user: user, params: expected_params).once.and_return(finder)
+ allow(finder).to receive(:execute).once.and_return([:execute_return_value])
+
+ expect(resolve_scope.items.to_a).to contain_exactly :execute_return_value
+ end
+ end
end
end
end
diff --git a/spec/graphql/resolvers/container_repository_tags_resolver_spec.rb b/spec/graphql/resolvers/container_repository_tags_resolver_spec.rb
index 0408357e8f2..48be1c29184 100644
--- a/spec/graphql/resolvers/container_repository_tags_resolver_spec.rb
+++ b/spec/graphql/resolvers/container_repository_tags_resolver_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Resolvers::ContainerRepositoryTagsResolver do
+RSpec.describe Resolvers::ContainerRepositoryTagsResolver, feature_category: :container_registry do
include GraphqlHelpers
let_it_be(:user) { create(:user) }
@@ -12,51 +12,135 @@ RSpec.describe Resolvers::ContainerRepositoryTagsResolver do
let(:args) { { sort: nil } }
describe '#resolve' do
- let(:resolver) do
- resolve(
- described_class,
- ctx: { current_user: user },
- obj: repository,
- args: args,
- arg_style: :internal
- )
+ shared_examples 'fetching via tags and filter in place' do
+ context 'by name' do
+ subject { resolver(args).map(&:name) }
+
+ before do
+ stub_container_registry_tags(repository: repository.path, tags: %w[aaa bab bbb ccc 123], with_manifest: false)
+ end
+
+ context 'without sort' do
+ # order is not guaranteed
+ it { is_expected.to contain_exactly('aaa', 'bab', 'bbb', 'ccc', '123') }
+ end
+
+ context 'with sorting and filtering' do
+ context 'name_asc' do
+ let(:args) { { sort: 'NAME_ASC' } }
+
+ it { is_expected.to eq(%w[123 aaa bab bbb ccc]) }
+ end
+
+ context 'name_desc' do
+ let(:args) { { sort: 'NAME_DESC' } }
+
+ it { is_expected.to eq(%w[ccc bbb bab aaa 123]) }
+ end
+
+ context 'filter by name' do
+ let(:args) { { sort: 'NAME_DESC', name: 'b' } }
+
+ it { is_expected.to eq(%w[bbb bab]) }
+ end
+ end
+ end
end
before do
stub_container_registry_config(enabled: true)
end
- context 'by name' do
- subject { resolver.map(&:name) }
-
+ context 'when Gitlab API is supported', :saas do
before do
- stub_container_registry_tags(repository: repository.path, tags: %w[aaa bab bbb ccc 123], with_manifest: false)
+ allow(repository).to receive(:tags_page).and_return({
+ tags: [],
+ pagination: {
+ previous: { uri: URI('/test?before=prev-cursor') },
+ next: { uri: URI('/test?last=next-cursor') }
+ }
+ })
+
+ allow(ContainerRegistry::GitlabApiClient).to receive(:supports_gitlab_api?).and_return(true)
end
- context 'without sort' do
- # order is not guaranteed
- it { is_expected.to contain_exactly('aaa', 'bab', 'bbb', 'ccc', '123') }
+ context 'get the page size based on first and last param' do
+ it 'sends the page size based on first if next page is asked' do
+ args = { sort: 'NAME_ASC', first: 10 }
+ expect(repository).to receive(:tags_page).with(hash_including(page_size: args[:first]))
+
+ resolver(args)
+ end
+
+ it 'sends the page size based on last if prev page is asked' do
+ args = { sort: 'NAME_ASC', last: 10 }
+ expect(repository).to receive(:tags_page).with(hash_including(page_size: args[:last]))
+
+ resolver(args)
+ end
end
- context 'with sorting and filtering' do
- context "name_asc" do
- let(:args) { { sort: :name_asc } }
+ context 'with parameters' do
+ using RSpec::Parameterized::TableSyntax
- it { is_expected.to eq(%w[123 aaa bab bbb ccc]) }
+ where(:before, :after, :sort, :name, :first, :last, :sort_value) do
+ nil | nil | 'NAME_DESC' | '' | 10 | nil | '-name'
+ 'bb' | nil | 'NAME_ASC' | 'a' | nil | 5 | 'name'
+ nil | 'aa' | 'NAME_DESC' | 'a' | 10 | nil | '-name'
end
- context "name_desc" do
- let(:args) { { sort: :name_desc } }
+ with_them do
+ let(:args) do
+ { before: before, after: after, sort: sort, name: name, first: first, last: last }.compact
+ end
+
+ it 'calls ContainerRepository#tags_page with correct parameters' do
+ expect(repository).to receive(:tags_page).with(
+ before: before,
+ last: after,
+ sort: sort_value,
+ name: name,
+ page_size: [first, last].map(&:to_i).max
+ )
- it { is_expected.to eq(%w[ccc bbb bab aaa 123]) }
+ resolver(args)
+ end
end
+ end
+
+ it 'returns an ExternallyPaginatedArray' do
+ expect(Gitlab::Graphql::ExternallyPaginatedArray)
+ .to receive(:new).with('prev-cursor', 'next-cursor')
- context 'filter by name' do
- let(:args) { { sort: :name_desc, name: 'b' } }
+ expect(resolver(args)).is_a? Gitlab::Graphql::ExternallyPaginatedArray
+ end
- it { is_expected.to eq(%w[bbb bab]) }
+ context 'when feature use_repository_list_tags_on_graphql is disabled' do
+ before do
+ stub_feature_flags(use_repository_list_tags_on_graphql: false)
end
+
+ it_behaves_like 'fetching via tags and filter in place'
end
end
+
+ context 'when Gitlab API is not supported' do
+ before do
+ allow(ContainerRegistry::GitlabApiClient).to receive(:supports_gitlab_api?).and_return(false)
+ end
+
+ it_behaves_like 'fetching via tags and filter in place'
+ end
+
+ def resolver(args, opts = {})
+ field_options = {
+ owner: resolver_parent,
+ resolver: described_class,
+ connection_extension: Gitlab::Graphql::Extensions::ExternallyPaginatedArrayExtension
+ }.merge(opts)
+
+ field = ::Types::BaseField.from_options('field_value', **field_options)
+ resolve_field(field, repository, args: args, object_type: resolver_parent)
+ end
end
end
diff --git a/spec/graphql/resolvers/data_transfer/group_data_transfer_resolver_spec.rb b/spec/graphql/resolvers/data_transfer/group_data_transfer_resolver_spec.rb
index 4ea3d287454..b5bffbc8803 100644
--- a/spec/graphql/resolvers/data_transfer/group_data_transfer_resolver_spec.rb
+++ b/spec/graphql/resolvers/data_transfer/group_data_transfer_resolver_spec.rb
@@ -10,6 +10,7 @@ RSpec.describe Resolvers::DataTransfer::GroupDataTransferResolver, feature_categ
let(:from) { Date.new(2022, 1, 1) }
let(:to) { Date.new(2023, 1, 1) }
+ let(:finder) { instance_double(::DataTransfer::GroupDataTransferFinder) }
let(:finder_results) do
[
build(:project_data_transfer, date: to, repository_egress: 250000)
@@ -41,21 +42,12 @@ RSpec.describe Resolvers::DataTransfer::GroupDataTransferResolver, feature_categ
include_examples 'Data transfer resolver'
- context 'when data_transfer_monitoring_mock_data is disabled' do
- let(:finder) { instance_double(::DataTransfer::GroupDataTransferFinder) }
+ it 'calls GroupDataTransferFinder with expected arguments' do
+ expect(::DataTransfer::GroupDataTransferFinder).to receive(:new).with(
+ group: group, from: from, to: to, user: current_user).once.and_return(finder)
+ allow(finder).to receive(:execute).once.and_return(finder_results)
- before do
- stub_feature_flags(data_transfer_monitoring_mock_data: false)
- end
-
- it 'calls GroupDataTransferFinder with expected arguments' do
- expect(::DataTransfer::GroupDataTransferFinder).to receive(:new).with(
- group: group, from: from, to: to, user: current_user
- ).once.and_return(finder)
- allow(finder).to receive(:execute).once.and_return(finder_results)
-
- expect(resolve_egress).to eq({ egress_nodes: finder_results.map(&:attributes) })
- end
+ expect(resolve_egress).to eq({ egress_nodes: finder_results.map(&:attributes) })
end
end
diff --git a/spec/graphql/resolvers/data_transfer/project_data_transfer_resolver_spec.rb b/spec/graphql/resolvers/data_transfer/project_data_transfer_resolver_spec.rb
index 7307c1a54a9..25ff02218cf 100644
--- a/spec/graphql/resolvers/data_transfer/project_data_transfer_resolver_spec.rb
+++ b/spec/graphql/resolvers/data_transfer/project_data_transfer_resolver_spec.rb
@@ -10,6 +10,9 @@ RSpec.describe Resolvers::DataTransfer::ProjectDataTransferResolver, feature_cat
let(:from) { Date.new(2022, 1, 1) }
let(:to) { Date.new(2023, 1, 1) }
+
+ let(:finder) { instance_double(::DataTransfer::ProjectDataTransferFinder) }
+
let(:finder_results) do
[
{
@@ -44,21 +47,12 @@ RSpec.describe Resolvers::DataTransfer::ProjectDataTransferResolver, feature_cat
include_examples 'Data transfer resolver'
- context 'when data_transfer_monitoring_mock_data is disabled' do
- let(:finder) { instance_double(::DataTransfer::ProjectDataTransferFinder) }
-
- before do
- stub_feature_flags(data_transfer_monitoring_mock_data: false)
- end
-
- it 'calls ProjectDataTransferFinder with expected arguments' do
- expect(::DataTransfer::ProjectDataTransferFinder).to receive(:new).with(
- project: project, from: from, to: to, user: current_user
- ).once.and_return(finder)
- allow(finder).to receive(:execute).once.and_return(finder_results)
+ it 'calls ProjectDataTransferFinder with expected arguments' do
+ expect(::DataTransfer::ProjectDataTransferFinder).to receive(:new).with(
+ project: project, from: from, to: to, user: current_user).once.and_return(finder)
+ allow(finder).to receive(:execute).once.and_return(finder_results)
- expect(resolve_egress).to eq({ egress_nodes: finder_results })
- end
+ expect(resolve_egress).to eq({ egress_nodes: finder_results })
end
end
diff --git a/spec/graphql/resolvers/projects_resolver_spec.rb b/spec/graphql/resolvers/projects_resolver_spec.rb
index 058d46a5e86..d9c1527af86 100644
--- a/spec/graphql/resolvers/projects_resolver_spec.rb
+++ b/spec/graphql/resolvers/projects_resolver_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Resolvers::ProjectsResolver do
+RSpec.describe Resolvers::ProjectsResolver, feature_category: :source_code_management do
include GraphqlHelpers
describe '#resolve' do
diff --git a/spec/graphql/resolvers/saved_reply_resolver_spec.rb b/spec/graphql/resolvers/saved_reply_resolver_spec.rb
index f1cb0ca5214..d6457e8c21a 100644
--- a/spec/graphql/resolvers/saved_reply_resolver_spec.rb
+++ b/spec/graphql/resolvers/saved_reply_resolver_spec.rb
@@ -8,30 +8,18 @@ RSpec.describe Resolvers::SavedReplyResolver, feature_category: :user_profile do
let_it_be(:current_user) { create(:user) }
let_it_be(:saved_reply) { create(:saved_reply, user: current_user) }
- describe 'feature flag disabled' do
- before do
- stub_feature_flags(saved_replies: false)
- end
-
- it 'does not return saved reply' do
- expect(resolve_saved_reply).to be_nil
- end
+ it 'returns users saved reply' do
+ expect(resolve_saved_reply).to eq(saved_reply)
end
- describe 'feature flag enabled' do
- it 'returns users saved reply' do
- expect(resolve_saved_reply).to eq(saved_reply)
- end
-
- it 'returns nil when saved reply is not found' do
- expect(resolve_saved_reply({ id: 'gid://gitlab/Users::SavedReply/100' })).to be_nil
- end
+ it 'returns nil when saved reply is not found' do
+ expect(resolve_saved_reply({ id: 'gid://gitlab/Users::SavedReply/100' })).to be_nil
+ end
- it 'returns nil when saved reply is another users' do
- other_users_saved_reply = create(:saved_reply, user: create(:user))
+ it 'returns nil when saved reply is another users' do
+ other_users_saved_reply = create(:saved_reply, user: create(:user))
- expect(resolve_saved_reply({ id: other_users_saved_reply.to_global_id })).to be_nil
- end
+ expect(resolve_saved_reply({ id: other_users_saved_reply.to_global_id })).to be_nil
end
def resolve_saved_reply(args = { id: saved_reply.to_global_id })
diff --git a/spec/graphql/resolvers/users/frecent_groups_resolver_spec.rb b/spec/graphql/resolvers/users/frecent_groups_resolver_spec.rb
new file mode 100644
index 00000000000..8836ba73110
--- /dev/null
+++ b/spec/graphql/resolvers/users/frecent_groups_resolver_spec.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Resolvers::Users::FrecentGroupsResolver, feature_category: :navigation do
+ it_behaves_like 'namespace visits resolver'
+end
diff --git a/spec/graphql/resolvers/users/frecent_projects_resolver_spec.rb b/spec/graphql/resolvers/users/frecent_projects_resolver_spec.rb
new file mode 100644
index 00000000000..30a0a7d93b3
--- /dev/null
+++ b/spec/graphql/resolvers/users/frecent_projects_resolver_spec.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Resolvers::Users::FrecentProjectsResolver, feature_category: :navigation do
+ it_behaves_like 'namespace visits resolver'
+end
diff --git a/spec/graphql/types/analytics/cycle_analytics/value_stream_type_spec.rb b/spec/graphql/types/analytics/cycle_analytics/value_stream_type_spec.rb
new file mode 100644
index 00000000000..5e2638210d3
--- /dev/null
+++ b/spec/graphql/types/analytics/cycle_analytics/value_stream_type_spec.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Types::Analytics::CycleAnalytics::ValueStreamType, feature_category: :value_stream_management do
+ specify { expect(described_class.graphql_name).to eq('ValueStream') }
+
+ specify { expect(described_class).to require_graphql_authorizations(:read_cycle_analytics) }
+
+ specify { expect(described_class).to have_graphql_fields(:id, :name, :namespace, :project) }
+end
diff --git a/spec/graphql/types/base_argument_spec.rb b/spec/graphql/types/base_argument_spec.rb
index 0ce6aa3667d..99154c8c9a5 100644
--- a/spec/graphql/types/base_argument_spec.rb
+++ b/spec/graphql/types/base_argument_spec.rb
@@ -3,41 +3,14 @@
require 'spec_helper'
RSpec.describe Types::BaseArgument, feature_category: :api do
- let_it_be(:field) do
- Types::BaseField.new(name: 'field', type: String, null: true)
- end
-
- let(:base_args) { { name: 'test', type: String, required: false, owner: field } }
-
- def subject(args = {})
- described_class.new(**base_args.merge(args))
- end
-
- include_examples 'Gitlab-style deprecations'
-
- describe 'required argument declarations' do
- it 'accepts nullable, required arguments' do
- arguments = base_args.merge({ required: :nullable })
-
- expect { subject(arguments) }.not_to raise_error
+ include_examples 'Gitlab-style deprecations' do
+ let_it_be(:field) do
+ Types::BaseField.new(name: 'field', type: String, null: true)
end
- it 'accepts required, non-nullable arguments' do
- arguments = base_args.merge({ required: true })
-
- expect { subject(arguments) }.not_to raise_error
- end
-
- it 'accepts non-required arguments' do
- arguments = base_args.merge({ required: false })
-
- expect { subject(arguments) }.not_to raise_error
- end
-
- it 'accepts no required argument declaration' do
- arguments = base_args
-
- expect { subject(arguments) }.not_to raise_error
+ def subject(args = {})
+ base_args = { name: 'test', type: String, required: false, owner: field }
+ described_class.new(**base_args.merge(args))
end
end
end
diff --git a/spec/graphql/types/ci/catalog/resource_sort_enum_spec.rb b/spec/graphql/types/ci/catalog/resource_sort_enum_spec.rb
new file mode 100644
index 00000000000..1de324b6652
--- /dev/null
+++ b/spec/graphql/types/ci/catalog/resource_sort_enum_spec.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe GitlabSchema.types['CiCatalogResourceSort'], feature_category: :pipeline_composition do
+ it { expect(described_class.graphql_name).to eq('CiCatalogResourceSort') }
+
+ it 'exposes all the existing catalog resource sort orders' do
+ expect(described_class.values.keys).to include(
+ *%w[NAME_ASC NAME_DESC LATEST_RELEASED_AT_ASC LATEST_RELEASED_AT_DESC CREATED_ASC CREATED_DESC]
+ )
+ end
+end
diff --git a/spec/graphql/types/ci/catalog/resource_type_spec.rb b/spec/graphql/types/ci/catalog/resource_type_spec.rb
new file mode 100644
index 00000000000..5f5732c5237
--- /dev/null
+++ b/spec/graphql/types/ci/catalog/resource_type_spec.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Types::Ci::Catalog::ResourceType, feature_category: :pipeline_composition do
+ specify { expect(described_class.graphql_name).to eq('CiCatalogResource') }
+
+ it 'exposes the expected fields' do
+ expected_fields = %i[
+ id
+ name
+ description
+ icon
+ web_path
+ versions
+ latest_version
+ latest_released_at
+ star_count
+ forks_count
+ root_namespace
+ readme_html
+ open_issues_count
+ open_merge_requests_count
+ ]
+
+ expect(described_class).to have_graphql_fields(*expected_fields)
+ end
+end
diff --git a/spec/graphql/types/container_registry/protection/rule_access_level_enum_spec.rb b/spec/graphql/types/container_registry/protection/rule_access_level_enum_spec.rb
new file mode 100644
index 00000000000..295401f89f9
--- /dev/null
+++ b/spec/graphql/types/container_registry/protection/rule_access_level_enum_spec.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe GitlabSchema.types['ContainerRegistryProtectionRuleAccessLevel'], feature_category: :container_registry do
+ it 'exposes all options' do
+ expect(described_class.values.keys).to match_array(%w[DEVELOPER MAINTAINER OWNER])
+ end
+end
diff --git a/spec/graphql/types/container_registry/protection/rule_type_spec.rb b/spec/graphql/types/container_registry/protection/rule_type_spec.rb
new file mode 100644
index 00000000000..58b53af80fb
--- /dev/null
+++ b/spec/graphql/types/container_registry/protection/rule_type_spec.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe GitlabSchema.types['ContainerRegistryProtectionRule'], feature_category: :container_registry do
+ specify { expect(described_class.graphql_name).to eq('ContainerRegistryProtectionRule') }
+
+ specify { expect(described_class.description).to be_present }
+
+ specify { expect(described_class).to require_graphql_authorizations(:admin_container_image) }
+
+ describe 'id' do
+ subject { described_class.fields['id'] }
+
+ it { is_expected.to have_non_null_graphql_type(::Types::GlobalIDType[::ContainerRegistry::Protection::Rule]) }
+ end
+
+ describe 'container_path_pattern' do
+ subject { described_class.fields['containerPathPattern'] }
+
+ it { is_expected.to have_non_null_graphql_type(GraphQL::Types::String) }
+ end
+
+ describe 'push_protected_up_to_access_level' do
+ subject { described_class.fields['pushProtectedUpToAccessLevel'] }
+
+ it { is_expected.to have_non_null_graphql_type(Types::ContainerRegistry::Protection::RuleAccessLevelEnum) }
+ end
+
+ describe 'delete_protected_up_to_access_level' do
+ subject { described_class.fields['deleteProtectedUpToAccessLevel'] }
+
+ it { is_expected.to have_non_null_graphql_type(Types::ContainerRegistry::Protection::RuleAccessLevelEnum) }
+ end
+end
diff --git a/spec/graphql/types/data_transfer/project_data_transfer_type_spec.rb b/spec/graphql/types/data_transfer/project_data_transfer_type_spec.rb
index a93da279b7f..80ead81650e 100644
--- a/spec/graphql/types/data_transfer/project_data_transfer_type_spec.rb
+++ b/spec/graphql/types/data_transfer/project_data_transfer_type_spec.rb
@@ -14,25 +14,15 @@ RSpec.describe GitlabSchema.types['ProjectDataTransfer'], feature_category: :sou
let_it_be(:project) { create(:project) }
let(:from) { Date.new(2022, 1, 1) }
let(:to) { Date.new(2023, 1, 1) }
- let(:finder_result) { 40_000_000 }
+ let(:relation) { instance_double(ActiveRecord::Relation) }
- it 'returns mock data' do
- expect(resolve_field(:total_egress, { from: from, to: to }, extras: { parent: project },
- arg_style: :internal)).to eq(finder_result)
+ before do
+ allow(relation).to receive(:sum).and_return(10)
end
- context 'when data_transfer_monitoring_mock_data is disabled' do
- let(:relation) { instance_double(ActiveRecord::Relation) }
-
- before do
- allow(relation).to receive(:sum).and_return(10)
- stub_feature_flags(data_transfer_monitoring_mock_data: false)
- end
-
- it 'calls sum on active record relation' do
- expect(resolve_field(:total_egress, { egress_nodes: relation }, extras: { parent: project },
- arg_style: :internal)).to eq(10)
- end
+ it 'calls sum on active record relation' do
+ expect(resolve_field(:total_egress, { egress_nodes: relation }, extras: { parent: project },
+ arg_style: :internal)).to eq(10)
end
end
end
diff --git a/spec/graphql/types/merge_request_review_state_enum_spec.rb b/spec/graphql/types/merge_request_review_state_enum_spec.rb
index 486e1c4f502..d8de3fcd1d1 100644
--- a/spec/graphql/types/merge_request_review_state_enum_spec.rb
+++ b/spec/graphql/types/merge_request_review_state_enum_spec.rb
@@ -6,12 +6,16 @@ RSpec.describe GitlabSchema.types['MergeRequestReviewState'] do
it 'the correct enum members' do
expect(described_class.values).to match(
'REVIEWED' => have_attributes(
- description: 'The merge request is reviewed.',
+ description: 'Merge request reviewer has reviewed.',
value: 'reviewed'
),
'UNREVIEWED' => have_attributes(
- description: 'The merge request is unreviewed.',
+ description: 'Awaiting review from merge request reviewer.',
value: 'unreviewed'
+ ),
+ 'REQUESTED_CHANGES' => have_attributes(
+ description: 'Merge request reviewer has requested changes.',
+ value: 'requested_changes'
)
)
end
diff --git a/spec/graphql/types/notes/noteable_interface_spec.rb b/spec/graphql/types/notes/noteable_interface_spec.rb
index e11dece60b8..c88cfe18b81 100644
--- a/spec/graphql/types/notes/noteable_interface_spec.rb
+++ b/spec/graphql/types/notes/noteable_interface_spec.rb
@@ -19,6 +19,7 @@ RSpec.describe Types::Notes::NoteableInterface do
expect(described_class.resolve_type(build(:merge_request), {})).to eq(Types::MergeRequestType)
expect(described_class.resolve_type(build(:design), {})).to eq(Types::DesignManagement::DesignType)
expect(described_class.resolve_type(build(:alert_management_alert), {})).to eq(Types::AlertManagement::AlertType)
+ expect(described_class.resolve_type(build(:abuse_report), {})).to eq(Types::AbuseReportType)
end
end
end
diff --git a/spec/graphql/types/organizations/organization_type_spec.rb b/spec/graphql/types/organizations/organization_type_spec.rb
index 26d7c10a715..62787ad220d 100644
--- a/spec/graphql/types/organizations/organization_type_spec.rb
+++ b/spec/graphql/types/organizations/organization_type_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe GitlabSchema.types['Organization'], feature_category: :cell do
- let(:expected_fields) { %w[groups id name organization_users path] }
+ let(:expected_fields) { %w[groups id name organization_users path web_url] }
specify { expect(described_class.graphql_name).to eq('Organization') }
specify { expect(described_class).to require_graphql_authorizations(:read_organization) }
diff --git a/spec/graphql/types/organizations/organization_user_badge_type_spec.rb b/spec/graphql/types/organizations/organization_user_badge_type_spec.rb
new file mode 100644
index 00000000000..1ea9b3ad1df
--- /dev/null
+++ b/spec/graphql/types/organizations/organization_user_badge_type_spec.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe GitlabSchema.types['OrganizationUserBadge'], feature_category: :cell do
+ let(:expected_fields) { %w[text variant] }
+
+ specify { expect(described_class.graphql_name).to eq('OrganizationUserBadge') }
+ specify { expect(described_class).to have_graphql_fields(*expected_fields) }
+end
diff --git a/spec/graphql/types/packages/package_base_type_spec.rb b/spec/graphql/types/packages/package_base_type_spec.rb
index ebe29da0539..6b568f4ae7f 100644
--- a/spec/graphql/types/packages/package_base_type_spec.rb
+++ b/spec/graphql/types/packages/package_base_type_spec.rb
@@ -4,8 +4,8 @@ require 'spec_helper'
RSpec.describe GitlabSchema.types['PackageBase'], feature_category: :package_registry do
specify { expect(described_class.description).to eq('Represents a package in the Package Registry') }
-
specify { expect(described_class).to require_graphql_authorizations(:read_package) }
+ specify { expect(described_class).to expose_permissions_using(Types::PermissionTypes::Package) }
it 'includes all expected fields' do
expected_fields = %w[
@@ -14,6 +14,7 @@ RSpec.describe GitlabSchema.types['PackageBase'], feature_category: :package_reg
project
tags metadata
status status_message can_destroy
+ user_permissions
]
expect(described_class).to include_graphql_fields(*expected_fields)
diff --git a/spec/graphql/types/packages/package_details_type_spec.rb b/spec/graphql/types/packages/package_details_type_spec.rb
index e4fe53f7660..464e81c0a8c 100644
--- a/spec/graphql/types/packages/package_details_type_spec.rb
+++ b/spec/graphql/types/packages/package_details_type_spec.rb
@@ -2,17 +2,17 @@
require 'spec_helper'
-RSpec.describe GitlabSchema.types['PackageDetailsType'] do
+RSpec.describe GitlabSchema.types['PackageDetailsType'], feature_category: :package_registry do
specify { expect(described_class.description).to eq('Represents a package details in the Package Registry') }
-
specify { expect(described_class).to require_graphql_authorizations(:read_package) }
+ specify { expect(described_class).to expose_permissions_using(Types::PermissionTypes::Package) }
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 public_package
npm_url maven_url conan_url nuget_url pypi_url pypi_setup_url
- composer_url composer_config_repository_url
+ composer_url composer_config_repository_url user_permissions
]
expect(described_class).to include_graphql_fields(*expected_fields)
diff --git a/spec/graphql/types/packages/package_type_spec.rb b/spec/graphql/types/packages/package_type_spec.rb
index df8135ed87e..dc1cc6a8bad 100644
--- a/spec/graphql/types/packages/package_type_spec.rb
+++ b/spec/graphql/types/packages/package_type_spec.rb
@@ -2,10 +2,10 @@
require 'spec_helper'
-RSpec.describe GitlabSchema.types['Package'] do
+RSpec.describe GitlabSchema.types['Package'], feature_category: :package_registry do
specify { expect(described_class.description).to eq('Represents a package with pipelines in the Package Registry') }
-
specify { expect(described_class).to require_graphql_authorizations(:read_package) }
+ specify { expect(described_class).to expose_permissions_using(Types::PermissionTypes::Package) }
it 'includes all the package fields and pipelines' do
expected_fields = %w[
@@ -14,6 +14,7 @@ RSpec.describe GitlabSchema.types['Package'] do
project
tags pipelines metadata
status can_destroy
+ user_permissions
]
expect(described_class).to include_graphql_fields(*expected_fields)
diff --git a/spec/graphql/types/packages/protection/rule_type_spec.rb b/spec/graphql/types/packages/protection/rule_type_spec.rb
index a4a458d3568..bc5a052796d 100644
--- a/spec/graphql/types/packages/protection/rule_type_spec.rb
+++ b/spec/graphql/types/packages/protection/rule_type_spec.rb
@@ -9,6 +9,12 @@ RSpec.describe GitlabSchema.types['PackagesProtectionRule'], feature_category: :
specify { expect(described_class).to require_graphql_authorizations(:admin_package) }
+ describe 'id' do
+ subject { described_class.fields['id'] }
+
+ it { is_expected.to have_non_null_graphql_type(::Types::GlobalIDType[::Packages::Protection::Rule]) }
+ end
+
describe 'package_name_pattern' do
subject { described_class.fields['packageNamePattern'] }
diff --git a/spec/graphql/types/packages/pypi/metadatum_type_spec.rb b/spec/graphql/types/packages/pypi/metadatum_type_spec.rb
index 16fb3ef2098..831307490a9 100644
--- a/spec/graphql/types/packages/pypi/metadatum_type_spec.rb
+++ b/spec/graphql/types/packages/pypi/metadatum_type_spec.rb
@@ -5,7 +5,14 @@ require 'spec_helper'
RSpec.describe GitlabSchema.types['PypiMetadata'] do
it 'includes pypi metadatum fields' do
expected_fields = %w[
- id required_python
+ author_email
+ description
+ description_content_type
+ id
+ keywords
+ metadata_version
+ required_python
+ summary
]
expect(described_class).to include_graphql_fields(*expected_fields)
diff --git a/spec/graphql/types/permission_types/abuse_report_spec.rb b/spec/graphql/types/permission_types/abuse_report_spec.rb
new file mode 100644
index 00000000000..399df137a78
--- /dev/null
+++ b/spec/graphql/types/permission_types/abuse_report_spec.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Types::PermissionTypes::AbuseReport, feature_category: :insider_threat do
+ it do
+ expected_permissions = [
+ :read_abuse_report, :create_note
+ ]
+
+ expected_permissions.each do |permission|
+ expect(described_class).to have_graphql_field(permission)
+ end
+ end
+end
diff --git a/spec/graphql/types/permission_types/ci/job_spec.rb b/spec/graphql/types/permission_types/ci/job_spec.rb
index e4bc5419070..238f086c7ee 100644
--- a/spec/graphql/types/permission_types/ci/job_spec.rb
+++ b/spec/graphql/types/permission_types/ci/job_spec.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe Types::PermissionTypes::Ci::Job do
it 'has expected permission fields' do
expected_permissions = [
- :read_job_artifacts, :read_build, :update_build
+ :read_job_artifacts, :read_build, :update_build, :cancel_build
]
expect(described_class).to have_graphql_fields(expected_permissions).only
diff --git a/spec/graphql/types/permission_types/ci/pipeline_spec.rb b/spec/graphql/types/permission_types/ci/pipeline_spec.rb
new file mode 100644
index 00000000000..6830b659b12
--- /dev/null
+++ b/spec/graphql/types/permission_types/ci/pipeline_spec.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Types::PermissionTypes::Ci::Pipeline, feature_category: :continuous_integration do
+ it 'has expected permission fields' do
+ expected_permissions = [
+ :admin_pipeline, :destroy_pipeline, :update_pipeline, :cancel_pipeline
+ ]
+
+ expect(described_class).to have_graphql_fields(expected_permissions).only
+ end
+end
diff --git a/spec/graphql/types/permission_types/package_spec.rb b/spec/graphql/types/permission_types/package_spec.rb
new file mode 100644
index 00000000000..3de37438234
--- /dev/null
+++ b/spec/graphql/types/permission_types/package_spec.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe GitlabSchema.types['PackagePermissions'], feature_category: :package_registry do
+ it 'has the expected fields' do
+ expected_permissions = [:destroy_package]
+
+ expect(described_class).to have_graphql_fields(expected_permissions).only
+ end
+end
diff --git a/spec/graphql/types/project_type_spec.rb b/spec/graphql/types/project_type_spec.rb
index e295014a2a6..7b4bcf4b1b0 100644
--- a/spec/graphql/types/project_type_spec.rb
+++ b/spec/graphql/types/project_type_spec.rb
@@ -39,7 +39,7 @@ RSpec.describe GitlabSchema.types['Project'] do
recent_issue_boards ci_config_path_or_default packages_cleanup_policy ci_variables
timelog_categories fork_targets branch_rules ci_config_variables pipeline_schedules languages
incident_management_timeline_event_tags visible_forks inherited_ci_variables autocomplete_users
- ci_cd_settings
+ ci_cd_settings detailed_import_status
]
expect(described_class).to include_graphql_fields(*expected_fields)
@@ -932,4 +932,93 @@ RSpec.describe GitlabSchema.types['Project'] do
end
end
end
+
+ describe 'detailed_import_status' do
+ let_it_be_with_reload(:project) { create(:project, :with_import_url) }
+
+ let(:query) do
+ %(
+ query {
+ project(fullPath: "#{project.full_path}") {
+ detailedImportStatus {
+ status
+ url
+ lastError
+ }
+ }
+ }
+ )
+ end
+
+ subject { GitlabSchema.execute(query, context: { current_user: current_user }).as_json }
+
+ let(:detailed_import_status) do
+ subject.dig('data', 'project', 'detailedImportStatus')
+ end
+
+ context 'when project is not imported' do
+ let(:current_user) { create(:user) }
+
+ before do
+ project.add_developer(current_user)
+ project.import_state.destroy!
+ end
+
+ it 'returns nil' do
+ expect(detailed_import_status).to be_nil
+ end
+ end
+
+ context 'when current_user is not set' do
+ let(:current_user) { nil }
+
+ it 'returns nil' do
+ expect(detailed_import_status).to be_nil
+ end
+ end
+
+ context 'when current_user has no permission' do
+ let(:current_user) { create(:user) }
+
+ it 'returns nil' do
+ expect(detailed_import_status).to be_nil
+ end
+ end
+
+ context 'when current_user has limited permission' do
+ let(:current_user) { create(:user) }
+
+ before do
+ project.add_developer(current_user)
+ project.import_state.last_error = 'Some error'
+ project.import_state.save!
+ end
+
+ it 'returns detailed information' do
+ expect(detailed_import_status).to include(
+ 'status' => project.import_state.status,
+ 'url' => project.safe_import_url,
+ 'lastError' => nil
+ )
+ end
+ end
+
+ context 'when current_user has permission' do
+ let(:current_user) { create(:user) }
+
+ before do
+ project.add_maintainer(current_user)
+ project.import_state.last_error = 'Some error'
+ project.import_state.save!
+ end
+
+ it 'returns detailed information' do
+ expect(detailed_import_status).to include(
+ 'status' => project.import_state.status,
+ 'url' => project.safe_import_url,
+ 'lastError' => 'Some error'
+ )
+ end
+ end
+ end
end
diff --git a/spec/graphql/types/projects/detailed_import_status_type_spec.rb b/spec/graphql/types/projects/detailed_import_status_type_spec.rb
new file mode 100644
index 00000000000..cfdf06e4ab5
--- /dev/null
+++ b/spec/graphql/types/projects/detailed_import_status_type_spec.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe GitlabSchema.types['DetailedImportStatus'], feature_category: :importers do
+ include GraphqlHelpers
+
+ let(:fields) do
+ %w[
+ id
+ status
+ url
+ last_error
+ last_update_at
+ last_update_started_at
+ last_successful_update_at
+ ]
+ end
+
+ it { expect(described_class.graphql_name).to eq('DetailedImportStatus') }
+ it { expect(described_class).to have_graphql_fields(fields) }
+ it { expect(described_class).to require_graphql_authorizations(:read_project) }
+end
diff --git a/spec/graphql/types/security/codequality_reports_comparer/report_generation_status_enum_spec.rb b/spec/graphql/types/security/codequality_reports_comparer/report_generation_status_enum_spec.rb
new file mode 100644
index 00000000000..16a115c37e0
--- /dev/null
+++ b/spec/graphql/types/security/codequality_reports_comparer/report_generation_status_enum_spec.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe GitlabSchema.types['CodequalityReportsComparerReportGenerationStatus'], feature_category: :code_quality do
+ specify { expect(described_class.graphql_name).to eq('CodequalityReportsComparerReportGenerationStatus') }
+
+ it 'exposes all codequality report status values' do
+ expect(described_class.values.keys).to contain_exactly('PARSED', 'PARSING', 'ERROR')
+ end
+end
diff --git a/spec/graphql/types/security/codequality_reports_comparer/status_enum_spec.rb b/spec/graphql/types/security/codequality_reports_comparer/status_enum_spec.rb
index 6e5bdd1e91d..626873deffe 100644
--- a/spec/graphql/types/security/codequality_reports_comparer/status_enum_spec.rb
+++ b/spec/graphql/types/security/codequality_reports_comparer/status_enum_spec.rb
@@ -2,8 +2,8 @@
require 'spec_helper'
-RSpec.describe GitlabSchema.types['CodequalityReportsComparerReportStatus'], feature_category: :code_quality do
- specify { expect(described_class.graphql_name).to eq('CodequalityReportsComparerReportStatus') }
+RSpec.describe GitlabSchema.types['CodequalityReportsComparerStatus'], feature_category: :code_quality do
+ specify { expect(described_class.graphql_name).to eq('CodequalityReportsComparerStatus') }
it 'exposes all codequality report status values' do
expect(described_class.values.keys).to contain_exactly('SUCCESS', 'FAILED', 'NOT_FOUND')
diff --git a/spec/graphql/types/security/codequality_reports_comparer_type_spec.rb b/spec/graphql/types/security/codequality_reports_comparer_type_spec.rb
index 02f7a9d6925..fad43845b58 100644
--- a/spec/graphql/types/security/codequality_reports_comparer_type_spec.rb
+++ b/spec/graphql/types/security/codequality_reports_comparer_type_spec.rb
@@ -6,6 +6,6 @@ RSpec.describe GitlabSchema.types['CodequalityReportsComparer'], feature_categor
specify { expect(described_class.graphql_name).to eq('CodequalityReportsComparer') }
it 'has expected fields' do
- expect(described_class).to have_graphql_fields(:report)
+ expect(described_class).to have_graphql_fields(:status, :report)
end
end
diff --git a/spec/graphql/types/user_type_spec.rb b/spec/graphql/types/user_type_spec.rb
index af0f8a86b6c..457127f5bed 100644
--- a/spec/graphql/types/user_type_spec.rb
+++ b/spec/graphql/types/user_type_spec.rb
@@ -55,6 +55,7 @@ RSpec.describe GitlabSchema.types['User'], feature_category: :user_profile do
organization
jobTitle
createdAt
+ lastActivityOn
pronouns
ide
]
diff --git a/spec/graphql/types/work_items/linked_item_type_spec.rb b/spec/graphql/types/work_items/linked_item_type_spec.rb
index 7d7fda45ce4..8dc8a742790 100644
--- a/spec/graphql/types/work_items/linked_item_type_spec.rb
+++ b/spec/graphql/types/work_items/linked_item_type_spec.rb
@@ -10,4 +10,10 @@ RSpec.describe Types::WorkItems::LinkedItemType, feature_category: :portfolio_ma
expect(described_class).to have_graphql_fields(*expected_fields)
end
+
+ describe 'work_item' do
+ subject { described_class.fields['workItem'] }
+
+ it { is_expected.to have_nullable_graphql_type(Types::WorkItemType) }
+ end
end
diff --git a/spec/helpers/admin/components_helper_spec.rb b/spec/helpers/admin/components_helper_spec.rb
index bb590d003ad..b2107f99a34 100644
--- a/spec/helpers/admin/components_helper_spec.rb
+++ b/spec/helpers/admin/components_helper_spec.rb
@@ -11,6 +11,7 @@ RSpec.describe Admin::ComponentsHelper, feature_category: :database do
}
main[:ci] = { adapter_name: 'PostgreSQL', version: expected_version } if Gitlab::Database.has_config?(:ci)
main[:geo] = { adapter_name: 'PostgreSQL', version: expected_version } if Gitlab::Database.has_config?(:geo)
+ main[:jh] = { adapter_name: 'PostgreSQL', version: expected_version } if Gitlab::Database.has_config?(:jh)
main
end
diff --git a/spec/helpers/admin/user_actions_helper_spec.rb b/spec/helpers/admin/user_actions_helper_spec.rb
index 87d2308690c..abfdabf3413 100644
--- a/spec/helpers/admin/user_actions_helper_spec.rb
+++ b/spec/helpers/admin/user_actions_helper_spec.rb
@@ -2,7 +2,7 @@
require "spec_helper"
-RSpec.describe Admin::UserActionsHelper do
+RSpec.describe Admin::UserActionsHelper, feature_category: :user_management do
describe '#admin_actions' do
let_it_be(:current_user) { build(:user) }
@@ -29,13 +29,33 @@ RSpec.describe Admin::UserActionsHelper do
context 'the user is a standard user' do
let_it_be(:user) { create(:user) }
- it { is_expected.to contain_exactly("edit", "block", "ban", "deactivate", "delete", "delete_with_contributions") }
+ it do
+ is_expected.to contain_exactly(
+ "edit",
+ "block",
+ "ban",
+ "deactivate",
+ "delete",
+ "delete_with_contributions",
+ "trust"
+ )
+ end
end
context 'the user is an admin user' do
let_it_be(:user) { create(:user, :admin) }
- it { is_expected.to contain_exactly("edit", "block", "ban", "deactivate", "delete", "delete_with_contributions") }
+ it do
+ is_expected.to contain_exactly(
+ "edit",
+ "block",
+ "ban",
+ "deactivate",
+ "delete",
+ "delete_with_contributions",
+ "trust"
+ )
+ end
end
context 'the user is blocked by LDAP' do
@@ -59,7 +79,16 @@ RSpec.describe Admin::UserActionsHelper do
context 'the user is deactivated' do
let_it_be(:user) { create(:user, :deactivated) }
- it { is_expected.to contain_exactly("edit", "block", "ban", "activate", "delete", "delete_with_contributions") }
+ it do
+ is_expected.to contain_exactly(
+ "edit",
+ "block",
+ "ban",
+ "activate",
+ "delete",
+ "delete_with_contributions"
+ )
+ end
end
context 'the user is locked' do
@@ -77,7 +106,8 @@ RSpec.describe Admin::UserActionsHelper do
"deactivate",
"unlock",
"delete",
- "delete_with_contributions"
+ "delete_with_contributions",
+ "trust"
)
}
end
@@ -88,6 +118,21 @@ RSpec.describe Admin::UserActionsHelper do
it { is_expected.to contain_exactly("edit", "unban", "delete", "delete_with_contributions") }
end
+ context 'the user is trusted' do
+ let_it_be(:user) { create(:user, :trusted) }
+
+ it do
+ is_expected.to contain_exactly("edit",
+ "block",
+ "deactivate",
+ "ban",
+ "delete",
+ "delete_with_contributions",
+ "untrust"
+ )
+ end
+ end
+
context 'the current_user does not have permission to delete the user' do
let_it_be(:user) { build(:user) }
@@ -95,7 +140,7 @@ RSpec.describe Admin::UserActionsHelper do
allow(helper).to receive(:can?).with(current_user, :destroy_user, user).and_return(false)
end
- it { is_expected.to contain_exactly("edit", "block", "ban", "deactivate") }
+ it { is_expected.to contain_exactly("edit", "block", "ban", "deactivate", "trust") }
end
context 'the user is a sole owner of a group' do
@@ -106,7 +151,7 @@ RSpec.describe Admin::UserActionsHelper do
group.add_owner(user)
end
- it { is_expected.to contain_exactly("edit", "block", "ban", "deactivate", "delete_with_contributions") }
+ it { is_expected.to contain_exactly("edit", "block", "ban", "deactivate", "delete_with_contributions", "trust") }
end
context 'the user is a bot' do
diff --git a/spec/helpers/application_helper_spec.rb b/spec/helpers/application_helper_spec.rb
index 7cf64c6e049..d67d07a4f1e 100644
--- a/spec/helpers/application_helper_spec.rb
+++ b/spec/helpers/application_helper_spec.rb
@@ -637,6 +637,21 @@ RSpec.describe ApplicationHelper do
expect(discord).to eq('https://discord.com/users/1234567890123456789')
end
end
+
+ context 'when mastodon is set' do
+ let_it_be(:user) { build(:user) }
+ let(:mastodon) { mastodon_url(user) }
+
+ it 'returns an empty string if mastodon username is not set' do
+ expect(mastodon).to eq('')
+ end
+
+ it 'returns mastodon url when mastodon username is set' do
+ user.mastodon = '@robin@example.com'
+
+ expect(mastodon).to eq(external_redirect_path(url: 'https://example.com/@robin'))
+ end
+ end
end
describe '#gitlab_ui_form_for' do
@@ -740,14 +755,6 @@ RSpec.describe ApplicationHelper do
it { is_expected.not_to include('with-top-bar') }
end
end
-
- describe 'logged-out-marketing-header' do
- before do
- allow(helper).to receive(:current_user).and_return(nil)
- end
-
- it { is_expected.not_to include('logged-out-marketing-header') }
- end
end
describe '#dispensable_render' do
diff --git a/spec/helpers/auth_helper_spec.rb b/spec/helpers/auth_helper_spec.rb
index 40798b4c038..264137add8a 100644
--- a/spec/helpers/auth_helper_spec.rb
+++ b/spec/helpers/auth_helper_spec.rb
@@ -2,7 +2,9 @@
require "spec_helper"
-RSpec.describe AuthHelper do
+RSpec.describe AuthHelper, feature_category: :system_access do
+ include LoginHelpers
+
describe "button_based_providers" do
it 'returns all enabled providers from devise' do
allow(helper).to receive(:auth_providers) { [:twitter, :github] }
@@ -310,88 +312,16 @@ RSpec.describe AuthHelper do
end
end
- describe '#auth_strategy_class' do
- subject(:auth_strategy_class) { helper.auth_strategy_class(name) }
-
- context 'when configuration specifies no provider' do
- let(:name) { 'does_not_exist' }
-
- before do
- allow(Gitlab.config.omniauth).to receive(:providers).and_return([])
- end
-
- it 'returns false' do
- expect(auth_strategy_class).to be_falsey
- end
- end
-
- context 'when configuration specifies a provider with args but without strategy_class' do
- let(:name) { 'google_oauth2' }
- let(:provider) do
- Struct.new(:name, :args).new(
- name,
- 'app_id' => 'YOUR_APP_ID'
- )
- end
-
- before do
- allow(Gitlab.config.omniauth).to receive(:providers).and_return([provider])
- end
-
- it 'returns false' do
- expect(auth_strategy_class).to be_falsey
- end
- end
-
- context 'when configuration specifies a provider with args and strategy_class' do
- let(:name) { 'provider1' }
- let(:strategy) { 'OmniAuth::Strategies::LDAP' }
- let(:provider) do
- Struct.new(:name, :args).new(
- name,
- 'strategy_class' => strategy
- )
- end
-
- before do
- allow(Gitlab.config.omniauth).to receive(:providers).and_return([provider])
- end
-
- it 'returns the class' do
- expect(auth_strategy_class).to eq(strategy)
- end
- end
-
- context 'when configuration specifies another provider with args and another strategy_class' do
- let(:name) { 'provider1' }
- let(:strategy) { 'OmniAuth::Strategies::LDAP' }
- let(:provider) do
- Struct.new(:name, :args).new(
- 'another_name',
- 'strategy_class' => strategy
- )
- end
-
- before do
- allow(Gitlab.config.omniauth).to receive(:providers).and_return([provider])
- end
-
- it 'returns false' do
- expect(auth_strategy_class).to be_falsey
- end
- end
- end
-
describe '#saml_providers' do
subject(:saml_providers) { helper.saml_providers }
let(:saml_strategy) { 'OmniAuth::Strategies::SAML' }
- let(:saml_provider_1_name) { 'saml_provider_1' }
+ let(:saml_provider_1_name) { 'saml' }
let(:saml_provider_1) do
Struct.new(:name, :args).new(
saml_provider_1_name,
- 'strategy_class' => saml_strategy
+ {}
)
end
@@ -422,7 +352,7 @@ RSpec.describe AuthHelper do
context 'when SAML is enabled without specifying a strategy class' do
before do
- allow(Gitlab::Auth::OAuth::Provider).to receive(:providers).and_return([:saml])
+ stub_omniauth_config(providers: [saml_provider_1])
end
it 'returns the saml provider' do
@@ -432,8 +362,7 @@ RSpec.describe AuthHelper do
context 'when configuration specifies no provider' do
before do
- allow(Devise).to receive(:omniauth_providers).and_return([])
- allow(Gitlab.config.omniauth).to receive(:providers).and_return([])
+ stub_omniauth_config(providers: [])
end
it 'returns an empty list' do
@@ -443,30 +372,27 @@ RSpec.describe AuthHelper do
context 'when configuration specifies a provider with a SAML strategy_class' do
before do
- allow(Devise).to receive(:omniauth_providers).and_return([saml_provider_1_name])
- allow(Gitlab.config.omniauth).to receive(:providers).and_return([saml_provider_1])
+ stub_omniauth_config(providers: [saml_provider_1])
end
it 'returns the provider' do
- expect(saml_providers).to match_array([saml_provider_1_name])
+ expect(saml_providers).to match_array([saml_provider_1_name.to_sym])
end
end
context 'when configuration specifies two providers with a SAML strategy_class' do
before do
- allow(Devise).to receive(:omniauth_providers).and_return([saml_provider_1_name, saml_provider_2_name])
- allow(Gitlab.config.omniauth).to receive(:providers).and_return([saml_provider_1, saml_provider_2])
+ stub_omniauth_config(providers: [saml_provider_1, saml_provider_2])
end
it 'returns the provider' do
- expect(saml_providers).to match_array([saml_provider_1_name, saml_provider_2_name])
+ expect(saml_providers).to match_array([saml_provider_1_name.to_sym, saml_provider_2_name.to_sym])
end
end
context 'when configuration specifies a provider with a non-SAML strategy_class' do
before do
- allow(Devise).to receive(:omniauth_providers).and_return([ldap_provider_name])
- allow(Gitlab.config.omniauth).to receive(:providers).and_return([ldap_provider])
+ stub_omniauth_config(providers: [ldap_provider])
end
it 'returns an empty list' do
@@ -476,12 +402,11 @@ RSpec.describe AuthHelper do
context 'when configuration specifies four providers but only two with SAML strategy_class' do
before do
- allow(Devise).to receive(:omniauth_providers).and_return([saml_provider_1_name, ldap_provider_name, saml_provider_2_name, google_oauth2_provider_name])
- allow(Gitlab.config.omniauth).to receive(:providers).and_return([saml_provider_1, ldap_provider, saml_provider_2, google_oauth2_provider])
+ stub_omniauth_config(providers: [saml_provider_1, ldap_provider, saml_provider_2, google_oauth2_provider])
end
it 'returns the provider' do
- expect(saml_providers).to match_array([saml_provider_1_name, saml_provider_2_name])
+ expect(saml_providers).to match_array([saml_provider_1_name.to_sym, saml_provider_2_name.to_sym])
end
end
end
diff --git a/spec/helpers/blob_helper_spec.rb b/spec/helpers/blob_helper_spec.rb
index e832fa2718a..d09e01c959c 100644
--- a/spec/helpers/blob_helper_spec.rb
+++ b/spec/helpers/blob_helper_spec.rb
@@ -221,7 +221,7 @@ RSpec.describe BlobHelper do
context 'when file is a pipeline config file' do
let(:data) { File.read(Rails.root.join('spec/support/gitlab_stubs/gitlab_ci.yml')) }
- let(:blob) { fake_blob(path: Gitlab::FileDetector::PATTERNS[:gitlab_ci], data: data) }
+ let(:blob) { fake_blob(path: project.ci_config_path_or_default, data: data) }
it 'is true' do
expect(helper.show_suggest_pipeline_creation_celebration?).to be_truthy
diff --git a/spec/helpers/ci/catalog/resources_helper_spec.rb b/spec/helpers/ci/catalog/resources_helper_spec.rb
index 3b29e6f292b..5c5d02ce6d8 100644
--- a/spec/helpers/ci/catalog/resources_helper_spec.rb
+++ b/spec/helpers/ci/catalog/resources_helper_spec.rb
@@ -5,17 +5,34 @@ require 'spec_helper'
RSpec.describe Ci::Catalog::ResourcesHelper, feature_category: :pipeline_composition do
include Devise::Test::ControllerHelpers
- let_it_be(:project) { build(:project) }
+ let_it_be(:project) { create_default(:project) }
+ let_it_be(:user) { create_default(:user) }
+
+ before do
+ allow(helper).to receive(:current_user).and_return(user)
+ end
describe '#can_add_catalog_resource?' do
subject { helper.can_add_catalog_resource?(project) }
- before do
- stub_licensed_features(ci_namespace_catalog: false)
+ context 'when user is not an owner' do
+ before_all do
+ project.add_maintainer(user)
+ end
+
+ it 'returns false' do
+ expect(subject).to be false
+ end
end
- it 'user cannot add a catalog resource in CE regardless of permissions' do
- expect(subject).to be false
+ context 'when user is an owner' do
+ before_all do
+ project.add_owner(user)
+ end
+
+ it 'returns true' do
+ expect(subject).to be true
+ end
end
end
diff --git a/spec/helpers/ci/jobs_helper_spec.rb b/spec/helpers/ci/jobs_helper_spec.rb
index 884fe7a018e..af369f7d420 100644
--- a/spec/helpers/ci/jobs_helper_spec.rb
+++ b/spec/helpers/ci/jobs_helper_spec.rb
@@ -43,6 +43,7 @@ RSpec.describe Ci::JobsHelper, feature_category: :continuous_integration do
"scheduled" => "SCHEDULED",
"skipped" => "SKIPPED",
"success" => "SUCCESS",
+ "waiting_for_callback" => "WAITING_FOR_CALLBACK",
"waiting_for_resource" => "WAITING_FOR_RESOURCE"
})
end
diff --git a/spec/helpers/ci/pipeline_editor_helper_spec.rb b/spec/helpers/ci/pipeline_editor_helper_spec.rb
index f411f533b25..dd7d602e2a5 100644
--- a/spec/helpers/ci/pipeline_editor_helper_spec.rb
+++ b/spec/helpers/ci/pipeline_editor_helper_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Ci::PipelineEditorHelper do
+RSpec.describe Ci::PipelineEditorHelper, feature_category: :pipeline_composition do
let_it_be(:project) { create(:project) }
let_it_be(:user) { create(:user) }
diff --git a/spec/helpers/ci/pipelines_helper_spec.rb b/spec/helpers/ci/pipelines_helper_spec.rb
index 477c07bf3e3..1a5c036b4f1 100644
--- a/spec/helpers/ci/pipelines_helper_spec.rb
+++ b/spec/helpers/ci/pipelines_helper_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Ci::PipelinesHelper do
+RSpec.describe Ci::PipelinesHelper, feature_category: :continuous_integration do
include Devise::Test::ControllerHelpers
describe 'pipeline_warnings' do
@@ -99,75 +99,6 @@ RSpec.describe Ci::PipelinesHelper do
:full_path,
:visibility_pipeline_id_type])
end
-
- describe 'when the project is eligible for the `ios_specific_templates` experiment' do
- let_it_be(:project) { create(:project, :auto_devops_disabled, shared_runners_enabled: false) }
- let_it_be(:user) { create(:user) }
-
- before do
- allow(helper).to receive(:current_user).and_return(user)
- project.add_developer(user)
- create(:project_setting, project: project, target_platforms: %w[ios])
- end
-
- describe 'the `registration_token` attribute' do
- subject { data[:registration_token] }
-
- context 'when the `ios_specific_templates` experiment variant is control' do
- before do
- stub_experiments(ios_specific_templates: :control)
- end
-
- it { is_expected.to be_nil }
- end
-
- context 'when the `ios_specific_templates` experiment variant is candidate' do
- before do
- stub_experiments(ios_specific_templates: :candidate)
- end
-
- context 'when the user cannot register project runners' do
- before do
- allow(helper).to receive(:can?).with(user, :register_project_runners, project).and_return(false)
- end
-
- it { is_expected.to be_nil }
- end
-
- context 'when the user can register project runners' do
- it { is_expected.to eq(project.runners_token) }
- end
- end
- end
-
- describe 'the `ios_runners_available` attribute', :saas do
- subject { data[:ios_runners_available] }
-
- context 'when the `ios_specific_templates` experiment variant is control' do
- before do
- stub_experiments(ios_specific_templates: :control)
- end
-
- it { is_expected.to be_nil }
- end
-
- context 'when the `ios_specific_templates` experiment variant is candidate' do
- before do
- stub_experiments(ios_specific_templates: :candidate)
- end
-
- context 'when shared runners are not enabled' do
- it { is_expected.to eq('false') }
- end
-
- context 'when shared runners are enabled' do
- let_it_be(:project) { create(:project, :auto_devops_disabled, shared_runners_enabled: true) }
-
- it { is_expected.to eq('true') }
- end
- end
- end
- end
end
describe '#visibility_pipeline_id_type' do
diff --git a/spec/helpers/ci/status_helper_spec.rb b/spec/helpers/ci/status_helper_spec.rb
index 17fe474b360..502a535e102 100644
--- a/spec/helpers/ci/status_helper_spec.rb
+++ b/spec/helpers/ci/status_helper_spec.rb
@@ -8,18 +8,6 @@ RSpec.describe Ci::StatusHelper do
let(:success_commit) { double("Ci::Pipeline", status: 'success') }
let(:failed_commit) { double("Ci::Pipeline", status: 'failed') }
- describe '#ci_icon_for_status' do
- it 'renders to correct svg on success' do
- expect(helper.ci_icon_for_status('success').to_s)
- .to include 'status_success'
- end
-
- it 'renders the correct svg on failure' do
- expect(helper.ci_icon_for_status('failed').to_s)
- .to include 'status_failed'
- end
- end
-
describe "#pipeline_status_cache_key" do
it "builds a cache key for pipeline status" do
pipeline_status = Gitlab::Cache::Ci::ProjectPipelineStatus.new(
@@ -33,23 +21,19 @@ RSpec.describe Ci::StatusHelper do
end
end
- describe "#render_status_with_link" do
- subject { helper.render_status_with_link("success") }
-
- it "renders a passed status icon" do
- is_expected.to include("<span class=\"ci-status-link ci-status-icon-success d-inline-flex")
- end
+ describe "#render_ci_icon" do
+ subject { helper.render_ci_icon("success") }
it "has 'Pipeline' as the status type in the title" do
is_expected.to include("title=\"Pipeline: passed\"")
end
it "has the success status icon" do
- is_expected.to include("ci-status-icon-success")
+ is_expected.to include("ci-icon-variant-success")
end
context "when pipeline has commit path" do
- subject { helper.render_status_with_link("success", "/commit-path") }
+ subject { helper.render_ci_icon("success", "/commit-path") }
it "links to commit" do
is_expected.to include("href=\"/commit-path\"")
@@ -60,53 +44,40 @@ RSpec.describe Ci::StatusHelper do
end
it "has the correct status icon" do
- is_expected.to include("ci-status-icon-success")
+ is_expected.to include("ci-icon-variant-success")
end
end
- context "when different type than pipeline is provided" do
- subject { helper.render_status_with_link("success", type: "commit") }
+ context "when showing status text" do
+ subject do
+ detailed_status = Gitlab::Ci::Status::Success.new(build(:ci_build, :success), build(:user))
+ helper.render_ci_icon(detailed_status, show_status_text: true)
+ end
- it "has the provided type in the title" do
- is_expected.to include("title=\"Commit: passed\"")
+ it "contains status text" do
+ is_expected.to include("data-testid=\"ci-icon-text\"")
+ is_expected.to include("passed")
end
end
context "when tooltip_placement is provided" do
- subject { helper.render_status_with_link("success", tooltip_placement: "right") }
+ subject { helper.render_ci_icon("success", tooltip_placement: "right") }
it "has the provided tooltip placement" do
is_expected.to include("data-placement=\"right\"")
end
end
- context "when additional CSS classes are provided" do
- subject { helper.render_status_with_link("success", cssclass: "extra-class") }
-
- it "has appended extra class to icon classes" do
- is_expected.to include('class="ci-status-link ci-status-icon-success d-inline-flex ' \
- 'gl-line-height-1 extra-class"')
- end
- end
-
context "when container is provided" do
- subject { helper.render_status_with_link("success", container: "my-container") }
+ subject { helper.render_ci_icon("success", container: "my-container") }
it "has the provided container in data" do
is_expected.to include("data-container=\"my-container\"")
end
end
- context "when icon_size is provided" do
- subject { helper.render_status_with_link("success", icon_size: 24) }
-
- it "has the svg class to change size" do
- is_expected.to include("<svg class=\"s24\"")
- end
- end
-
context "when status is success-with-warnings" do
- subject { helper.render_status_with_link("success-with-warnings") }
+ subject { helper.render_ci_icon("success-with-warnings") }
it "renders warning variant of gl-badge" do
is_expected.to include('gl-badge badge badge-pill badge-warning')
@@ -114,33 +85,41 @@ RSpec.describe Ci::StatusHelper do
end
context "when status is manual" do
- subject { helper.render_status_with_link("manual") }
+ subject { helper.render_ci_icon("manual") }
it "renders neutral variant of gl-badge" do
is_expected.to include('gl-badge badge badge-pill badge-neutral')
end
end
- end
- describe '#badge_variant' do
- using RSpec::Parameterized::TableSyntax
-
- where(:status, :expected_badge_variant_class) do
- 'success' | 'badge-success'
- 'success-with-warnings' | 'badge-warning'
- 'pending' | 'badge-warning'
- 'failed' | 'badge-danger'
- 'running' | 'badge-info'
- 'canceled' | 'badge-neutral'
- 'manual' | 'badge-neutral'
- 'other-status' | 'badge-muted'
- end
+ describe 'badge and icon appearance' do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:status, :icon, :badge_variant) do
+ 'success' | 'status_success_borderless' | 'success'
+ 'success-with-warnings' | 'status_warning_borderless' | 'warning'
+ 'pending' | 'status_pending_borderless' | 'warning'
+ 'waiting-for-resource' | 'status_pending_borderless' | 'warning'
+ 'failed' | 'status_failed_borderless' | 'danger'
+ 'running' | 'status_running_borderless' | 'info'
+ 'preparing' | 'status_preparing_borderless' | 'neutral'
+ 'canceled' | 'status_canceled_borderless' | 'neutral'
+ 'created' | 'status_created_borderless' | 'neutral'
+ 'scheduled' | 'status_scheduled_borderless' | 'neutral'
+ 'play' | 'play' | 'neutral'
+ 'skipped' | 'status_skipped_borderless' | 'neutral'
+ 'manual' | 'status_manual_borderless' | 'neutral'
+ 'other-status' | 'status_canceled_borderless' | 'neutral'
+ end
- with_them do
- subject { helper.render_status_with_link(status) }
+ with_them do
+ subject { helper.render_ci_icon(status) }
- it 'uses the correct badge variant classes for gl-badge' do
- is_expected.to include("gl-badge badge badge-pill #{expected_badge_variant_class}")
+ it 'uses the correct variant and icon for status' do
+ is_expected.to include("gl-badge badge badge-pill badge-#{badge_variant}")
+ is_expected.to include("ci-icon-variant-#{badge_variant}")
+ is_expected.to include("data-testid=\"#{icon}-icon\"")
+ end
end
end
end
diff --git a/spec/helpers/colors_helper_spec.rb b/spec/helpers/colors_helper_spec.rb
index ca5cafb7ebe..328796ef1ae 100644
--- a/spec/helpers/colors_helper_spec.rb
+++ b/spec/helpers/colors_helper_spec.rb
@@ -39,51 +39,4 @@ RSpec.describe ColorsHelper do
end
end
end
-
- describe '#rgb_array_to_hex_color' do
- context 'valid RGB array' do
- where(:rgb_array, :hex_color) do
- [0, 0, 0] | '#000000'
- [0, 0, 255] | '#0000ff'
- [0, 255, 0] | '#00ff00'
- [255, 0, 0] | '#ff0000'
- [12, 34, 56] | '#0c2238'
- [222, 111, 88] | '#de6f58'
- [255, 255, 255] | '#ffffff'
- end
-
- with_them do
- it 'returns correct hex color' do
- expect(helper.rgb_array_to_hex_color(rgb_array)).to eq(hex_color)
- end
- end
- end
-
- context 'invalid RGB array' do
- where(:rgb_array) do
- [
- '',
- '#000000',
- 0,
- nil,
- [],
- [0],
- [0, 0],
- [0, 0, 0, 0],
- [-1, 0, 0],
- [0, -1, 0],
- [0, 0, -1],
- [256, 0, 0],
- [0, 256, 0],
- [0, 0, 256]
- ]
- end
-
- with_them do
- it 'raise ArgumentError' do
- expect { helper.rgb_array_to_hex_color(rgb_array) }.to raise_error(ArgumentError)
- end
- end
- end
- end
end
diff --git a/spec/helpers/environment_helper_spec.rb b/spec/helpers/environment_helper_spec.rb
index 1383bf34881..0c04417fca6 100644
--- a/spec/helpers/environment_helper_spec.rb
+++ b/spec/helpers/environment_helper_spec.rb
@@ -3,45 +3,6 @@
require 'spec_helper'
RSpec.describe EnvironmentHelper, feature_category: :environment_management do
- describe '#render_deployment_status' do
- context 'when using a manual deployment' do
- it 'renders a span tag' do
- deploy = build(:deployment, deployable: nil, status: :success)
- html = helper.render_deployment_status(deploy)
-
- expect(html).to have_css('span.ci-status.ci-success')
- end
- end
-
- context 'when using a deployment from a build' do
- it 'renders a link tag' do
- deploy = build(:deployment, status: :success)
- html = helper.render_deployment_status(deploy)
-
- expect(html).to have_css('a.ci-status.ci-success')
- end
- end
-
- context 'when deploying from a bridge' do
- it 'renders a span tag' do
- deploy = build(:deployment, deployable: create(:ci_bridge), status: :success)
- html = helper.render_deployment_status(deploy)
-
- expect(html).to have_css('span.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
subject { helper.environments_detail_data_json(user, project, environment) }
diff --git a/spec/helpers/environments_helper_spec.rb b/spec/helpers/environments_helper_spec.rb
index 6624404bc49..14f99f144b2 100644
--- a/spec/helpers/environments_helper_spec.rb
+++ b/spec/helpers/environments_helper_spec.rb
@@ -23,8 +23,8 @@ RSpec.describe EnvironmentsHelper, feature_category: :environment_management do
'settings_path' => edit_project_settings_integration_path(project, 'prometheus'),
'clusters_path' => project_clusters_path(project),
'current_environment_name' => environment.name,
- 'documentation_path' => help_page_path('administration/monitoring/prometheus/index.md'),
- 'add_dashboard_documentation_path' => help_page_path('operations/metrics/dashboards/index.md', anchor: 'add-a-new-dashboard-to-your-project'),
+ 'documentation_path' => help_page_path('administration/monitoring/prometheus/index'),
+ 'add_dashboard_documentation_path' => help_page_path('operations/metrics/dashboards/index', anchor: 'add-a-new-dashboard-to-your-project'),
'empty_getting_started_svg_path' => match_asset_path('/assets/illustrations/monitoring/getting_started.svg'),
'empty_loading_svg_path' => match_asset_path('/assets/illustrations/monitoring/loading.svg'),
'empty_no_data_svg_path' => match_asset_path('/assets/illustrations/monitoring/no_data.svg'),
@@ -95,17 +95,4 @@ RSpec.describe EnvironmentsHelper, feature_category: :environment_management do
expect(subject).to eq(true)
end
end
-
- describe '#environment_logs_data' do
- it 'returns logs data' do
- expected_data = {
- "environment_name": environment.name,
- "environments_path": api_v4_projects_environments_path(id: project.id),
- "environment_id": environment.id,
- "clusters_path": project_clusters_path(project, format: :json)
- }
-
- expect(helper.environment_logs_data(project, environment)).to eq(expected_data)
- end
- end
end
diff --git a/spec/helpers/events_helper_spec.rb b/spec/helpers/events_helper_spec.rb
index 6ffca876361..d19df3d1395 100644
--- a/spec/helpers/events_helper_spec.rb
+++ b/spec/helpers/events_helper_spec.rb
@@ -22,7 +22,8 @@ RSpec.describe EventsHelper, factory_default: :keep, feature_category: :user_pro
it 'returns a link to the author' do
name = user.name
- expect(helper.link_to_author(event)).to eq(link_to(name, user_path(user.username), title: name))
+ expect(helper.link_to_author(event)).to eq(link_to(name, user_path(user.username), title: name,
+ data: { user_id: user.id, username: user.username }, class: 'js-user-link'))
end
it 'returns the author name if the author is not present' do
@@ -35,7 +36,71 @@ RSpec.describe EventsHelper, factory_default: :keep, feature_category: :user_pro
allow(helper).to receive(:current_user).and_return(user)
name = _('You')
- expect(helper.link_to_author(event, self_added: true)).to eq(link_to(name, user_path(user.username), title: name))
+ expect(helper.link_to_author(event, self_added: true)).to eq(link_to(name, user_path(user.username), title: name,
+ data: { user_id: user.id, username: user.username }, class: 'js-user-link'))
+ end
+ end
+
+ describe '#icon_for_profile_event' do
+ let(:event) { build(:event, :joined) }
+ let(:users_activity_page?) { true }
+
+ before do
+ allow(helper).to receive(:current_path?).and_call_original
+ allow(helper).to receive(:current_path?).with('users#activity').and_return(users_activity_page?)
+ end
+
+ context 'when on users activity page' do
+ it 'gives an icon with specialized classes' do
+ result = helper.icon_for_profile_event(event)
+
+ expect(result).to include('joined-icon')
+ expect(result).to include('<svg')
+ end
+
+ context 'with an unsupported event action_name' do
+ let(:event) { build(:event, :expired) }
+
+ it 'does not have an icon' do
+ result = helper.icon_for_profile_event(event)
+
+ expect(result).not_to include('<svg')
+ end
+ end
+ end
+
+ context 'when not on users activity page' do
+ let(:users_activity_page?) { false }
+
+ it 'gives an icon with specialized classes' do
+ result = helper.icon_for_profile_event(event)
+
+ expect(result).not_to include('joined-icon')
+ expect(result).not_to include('<svg')
+ expect(result).to include('<img')
+ end
+ end
+ end
+
+ describe '#event_user_info' do
+ let(:event) { build(:event) }
+ let(:users_activity_page?) { true }
+
+ before do
+ allow(helper).to receive(:current_path?).and_call_original
+ allow(helper).to receive(:current_path?).with('users#activity').and_return(users_activity_page?)
+ end
+
+ subject { helper.event_user_info(event) }
+
+ context 'when on users activity page' do
+ it { is_expected.to be_nil }
+ end
+
+ context 'when not on users activity page' do
+ let(:users_activity_page?) { false }
+
+ it { is_expected.to include('<div') }
end
end
@@ -268,12 +333,26 @@ RSpec.describe EventsHelper, factory_default: :keep, feature_category: :user_pro
describe '#event_wiki_title_html' do
let(:event) { create(:wiki_page_event) }
+ let(:url) { helper.event_wiki_page_target_url(event) }
+ let(:title) { event.target_title }
it 'produces a suitable title chunk' do
- url = helper.event_wiki_page_target_url(event)
- title = event.target_title
html = [
- "<span class=\"event-target-type gl-mr-2\">wiki page</span>",
+ "<span class=\"event-target-type gl-mr-2 \">wiki page</span>",
+ "<a title=\"#{title}\" class=\"has-tooltip event-target-link gl-mr-2\" href=\"#{url}\">",
+ title,
+ "</a>"
+ ].join
+
+ expect(helper.event_wiki_title_html(event)).to eq(html)
+ end
+
+ it 'produces a suitable title chunk on the user profile' do
+ allow(helper).to receive(:user_profile_activity_classes).and_return(
+ 'gl-font-weight-semibold gl-text-black-normal')
+
+ html = [
+ "<span class=\"event-target-type gl-mr-2 gl-font-weight-semibold gl-text-black-normal\">wiki page</span>",
"<a title=\"#{title}\" class=\"has-tooltip event-target-link gl-mr-2\" href=\"#{url}\">",
title,
"</a>"
@@ -441,5 +520,28 @@ RSpec.describe EventsHelper, factory_default: :keep, feature_category: :user_pro
end
end
end
+
+ describe '#user_profile_activity_classes' do
+ let(:users_activity_page?) { true }
+
+ before do
+ allow(helper).to receive(:current_path?).and_call_original
+ allow(helper).to receive(:current_path?).with('users#activity').and_return(users_activity_page?)
+ end
+
+ context 'when on the user activity page' do
+ it 'returns the expected class names' do
+ expect(helper.user_profile_activity_classes).to eq(' gl-font-weight-semibold gl-text-black-normal')
+ end
+ end
+
+ context 'when not on the user activity page' do
+ let(:users_activity_page?) { false }
+
+ it 'returns an empty string' do
+ expect(helper.user_profile_activity_classes).to eq('')
+ end
+ end
+ end
end
# rubocop:enable RSpec/FactoryBot/AvoidCreate
diff --git a/spec/helpers/groups/group_members_helper_spec.rb b/spec/helpers/groups/group_members_helper_spec.rb
index a9c6822e2c1..3a7c852b683 100644
--- a/spec/helpers/groups/group_members_helper_spec.rb
+++ b/spec/helpers/groups/group_members_helper_spec.rb
@@ -19,14 +19,9 @@ RSpec.describe Groups::GroupMembersHelper do
let(:members_collection) { members }
before do
- allow(helper).to receive(:can?).with(current_user, :export_group_memberships, group).and_return(false)
- allow(helper).to receive(:can?).with(current_user, :owner_access, group).and_return(true)
allow(helper).to receive(:current_user).and_return(current_user)
- allow(helper).to receive(:can?).with(current_user, :export_group_memberships, shared_group).and_return(true)
allow(helper).to receive(:group_group_member_path).with(shared_group, ':id').and_return('/groups/foo-bar/-/group_members/:id')
allow(helper).to receive(:group_group_link_path).with(shared_group, ':id').and_return('/groups/foo-bar/-/group_links/:id')
- allow(helper).to receive(:can?).with(current_user, :admin_group_member, shared_group).and_return(true)
- allow(helper).to receive(:can?).with(current_user, :admin_member_access_request, shared_group).and_return(true)
end
subject do
@@ -54,8 +49,8 @@ RSpec.describe Groups::GroupMembersHelper do
it 'returns expected json' do
expected = {
source_id: shared_group.id,
- can_manage_members: true,
- can_manage_access_requests: true,
+ can_manage_members: be_in([true, false]),
+ can_manage_access_requests: be_in([true, false]),
group_name: shared_group.name,
group_path: shared_group.full_path
}
@@ -102,9 +97,6 @@ RSpec.describe Groups::GroupMembersHelper do
before do
allow(helper).to receive(:group_group_member_path).with(sub_shared_group, ':id').and_return('/groups/foo-bar/-/group_members/:id')
allow(helper).to receive(:group_group_link_path).with(sub_shared_group, ':id').and_return('/groups/foo-bar/-/group_links/:id')
- allow(helper).to receive(:can?).with(current_user, :admin_group_member, sub_shared_group).and_return(true)
- allow(helper).to receive(:can?).with(current_user, :admin_member_access_request, sub_shared_group).and_return(true)
- allow(helper).to receive(:can?).with(current_user, :export_group_memberships, sub_shared_group).and_return(true)
end
subject do
diff --git a/spec/helpers/groups_helper_spec.rb b/spec/helpers/groups_helper_spec.rb
index 0db15541b99..f5cadfc2761 100644
--- a/spec/helpers/groups_helper_spec.rb
+++ b/spec/helpers/groups_helper_spec.rb
@@ -288,13 +288,13 @@ RSpec.describe GroupsHelper, feature_category: :groups_and_projects do
end
it 'returns false if parent group is disabling emails' do
- allow(group).to receive(:emails_disabled?).and_return(true)
+ allow(group).to receive(:emails_enabled?).and_return(false)
expect(helper.can_disable_group_emails?(subgroup)).to be_falsey
end
it 'returns true if parent group is not disabling emails' do
- allow(group).to receive(:emails_disabled?).and_return(false)
+ allow(group).to receive(:emails_enabled?).and_return(true)
expect(helper.can_disable_group_emails?(subgroup)).to be_truthy
end
diff --git a/spec/helpers/ide_helper_spec.rb b/spec/helpers/ide_helper_spec.rb
index 47500b8e21e..d5d7f8f72b3 100644
--- a/spec/helpers/ide_helper_spec.rb
+++ b/spec/helpers/ide_helper_spec.rb
@@ -101,7 +101,7 @@ RSpec.describe IdeHelper, feature_category: :web_ide do
'user-preferences-path' => profile_preferences_path,
'sign-in-path' => 'test-sign-in-path',
'new-web-ide-help-page-path' =>
- help_page_path('user/project/web_ide/index.md', anchor: 'vscode-reimplementation'),
+ help_page_path('user/project/web_ide/index', anchor: 'vscode-reimplementation'),
'csp-nonce' => 'test-csp-nonce',
'ide-remote-path' => ide_remote_path(remote_host: ':remote_host', remote_path: ':remote_path')
}
diff --git a/spec/helpers/issuables_helper_spec.rb b/spec/helpers/issuables_helper_spec.rb
index 0faea5629e8..6abce4c5983 100644
--- a/spec/helpers/issuables_helper_spec.rb
+++ b/spec/helpers/issuables_helper_spec.rb
@@ -109,10 +109,14 @@ RSpec.describe IssuablesHelper, feature_category: :team_planning do
allow(helper).to receive(:current_user).and_return(user)
end
- context 'when assigned issues count is over 100' do
- let_it_be(:issues) { create_list(:issue, 101, project: project, assignees: [user]) }
+ context 'when assigned issues count is over MAX_LIMIT_FOR_ASSIGNEED_ISSUES_COUNT' do
+ before do
+ stub_const('User::MAX_LIMIT_FOR_ASSIGNEED_ISSUES_COUNT', 2)
+ end
+
+ let_it_be(:issues) { create_list(:issue, 3, project: project, assignees: [user]) }
- it { is_expected.to eq 100 }
+ it { is_expected.to eq 2 }
end
end
end
@@ -127,10 +131,14 @@ RSpec.describe IssuablesHelper, feature_category: :team_planning do
allow(helper).to receive(:current_user).and_return(user)
end
- context 'when assigned issues count is over 99' do
- let_it_be(:issues) { create_list(:issue, 100, project: project, assignees: [user]) }
+ context 'when assigned issues count is over MAX_LIMIT_FOR_ASSIGNEED_ISSUES_COUNT' do
+ before do
+ stub_const('User::MAX_LIMIT_FOR_ASSIGNEED_ISSUES_COUNT', 2)
+ end
+
+ let_it_be(:issues) { create_list(:issue, 3, project: project, assignees: [user]) }
- it { is_expected.to eq '99+' }
+ it { is_expected.to eq '1+' }
end
end
diff --git a/spec/helpers/members_helper_spec.rb b/spec/helpers/members_helper_spec.rb
index 68a12d8dbf7..d2d758cdc7c 100644
--- a/spec/helpers/members_helper_spec.rb
+++ b/spec/helpers/members_helper_spec.rb
@@ -45,21 +45,6 @@ RSpec.describe MembersHelper do
end
end
- describe '#remove_member_title' do
- let(:requester) { create(:user) }
- let(:project) { create(:project, :public) }
- let(:project_member) { build(:project_member, project: project) }
- let(:project_member_request) { project.request_access(requester) }
- let(:group) { create(:group) }
- let(:group_member) { build(:group_member, group: group) }
- let(:group_member_request) { group.request_access(requester) }
-
- it { expect(remove_member_title(project_member)).to eq 'Remove user from project' }
- it { expect(remove_member_title(project_member_request)).to eq 'Deny access request from project' }
- it { expect(remove_member_title(group_member)).to eq 'Remove user from group and any subresources' }
- it { expect(remove_member_title(group_member_request)).to eq 'Deny access request from group' }
- end
-
describe '#leave_confirmation_message' do
let(:project) { build_stubbed(:project) }
let(:group) { build_stubbed(:group) }
diff --git a/spec/helpers/nav/new_dropdown_helper_spec.rb b/spec/helpers/nav/new_dropdown_helper_spec.rb
index 47f23c4fa21..4252e10c922 100644
--- a/spec/helpers/nav/new_dropdown_helper_spec.rb
+++ b/spec/helpers/nav/new_dropdown_helper_spec.rb
@@ -11,6 +11,7 @@ RSpec.describe Nav::NewDropdownHelper, feature_category: :navigation do
let(:with_can_create_project) { false }
let(:with_can_create_group) { false }
let(:with_can_create_snippet) { false }
+ let(:with_can_create_organization) { false }
let(:title) { 'Create new...' }
subject(:view_model) do
@@ -24,6 +25,7 @@ RSpec.describe Nav::NewDropdownHelper, feature_category: :navigation do
allow(user).to receive(:can_create_group?) { with_can_create_group }
allow(user).to receive(:can?).and_call_original
allow(user).to receive(:can?).with(:create_snippet) { with_can_create_snippet }
+ allow(user).to receive(:can?).with(:create_organization) { with_can_create_organization }
end
shared_examples 'invite member item' do |partial|
@@ -135,6 +137,39 @@ RSpec.describe Nav::NewDropdownHelper, feature_category: :navigation do
)
end
end
+
+ context 'when can create organization' do
+ let(:with_can_create_organization) { true }
+
+ it 'has new organization menu item' do
+ expect(view_model[:menu_sections]).to eq(
+ expected_menu_section(
+ title: _('In GitLab'),
+ menu_item: ::Gitlab::Nav::TopNavMenuItem.build(
+ id: 'general_new_organization',
+ title: s_('Organization|New organization'),
+ href: '/-/organizations/new',
+ data: {
+ track_action: 'click_link_new_organization_parent',
+ track_label: 'plus_menu_dropdown',
+ track_property: 'navigation_top',
+ testid: 'global_new_organization_link'
+ }
+ )
+ )
+ )
+ end
+
+ context 'when ui_for_organizations feature flag is disabled' do
+ before do
+ stub_feature_flags(ui_for_organizations: false)
+ end
+
+ it 'does not have new organization menu item' do
+ expect(view_model[:menu_sections]).to match_array([])
+ end
+ end
+ end
end
context 'with persisted group' do
diff --git a/spec/helpers/nav_helper_spec.rb b/spec/helpers/nav_helper_spec.rb
index 12ab7ca93c0..9a0f72838fb 100644
--- a/spec/helpers/nav_helper_spec.rb
+++ b/spec/helpers/nav_helper_spec.rb
@@ -145,20 +145,12 @@ RSpec.describe NavHelper, feature_category: :navigation do
let(:user_preference) { nil }
specify { expect(subject).to eq true }
-
- context 'when the user was not enrolled into the new nav via a special feature flag' do
- before do
- stub_feature_flags(super_sidebar_nav_enrolled: false)
- end
-
- specify { expect(subject).to eq false }
- end
end
context 'when user has new nav disabled' do
let(:user_preference) { false }
- specify { expect(subject).to eq false }
+ specify { expect(subject).to eq true }
end
context 'when user has new nav enabled' do
@@ -168,24 +160,6 @@ RSpec.describe NavHelper, feature_category: :navigation do
end
end
- shared_examples 'anonymous show_super_sidebar is supposed to' do
- before do
- stub_feature_flags(super_sidebar_logged_out: feature_flag)
- end
-
- context 'when super_sidebar_logged_out feature flag is disabled' do
- let(:feature_flag) { false }
-
- specify { expect(subject).to eq false }
- end
-
- context 'when super_sidebar_logged_out feature flag is enabled' do
- let(:feature_flag) { true }
-
- specify { expect(subject).to eq true }
- end
- end
-
context 'without a user' do
context 'with current_user (nil) as a default' do
before do
@@ -194,13 +168,13 @@ RSpec.describe NavHelper, feature_category: :navigation do
subject { helper.show_super_sidebar? }
- it_behaves_like 'anonymous show_super_sidebar is supposed to'
+ specify { expect(subject).to eq true }
end
context 'with nil provided as an argument' do
subject { helper.show_super_sidebar?(nil) }
- it_behaves_like 'anonymous show_super_sidebar is supposed to'
+ specify { expect(subject).to eq true }
end
end
diff --git a/spec/helpers/operations_helper_spec.rb b/spec/helpers/operations_helper_spec.rb
index 9e50712a386..e018ab41a83 100644
--- a/spec/helpers/operations_helper_spec.rb
+++ b/spec/helpers/operations_helper_spec.rb
@@ -30,7 +30,7 @@ RSpec.describe OperationsHelper do
it 'returns the correct values' do
expect(subject).to eq(
- 'alerts_setup_url' => help_page_path('operations/incident_management/integrations.md', anchor: 'configuration'),
+ 'alerts_setup_url' => help_page_path('operations/incident_management/integrations', anchor: 'configuration'),
'alerts_usage_url' => project_alert_management_index_path(project),
'prometheus_form_path' => project_settings_integration_path(project, prometheus_integration),
'prometheus_reset_key_path' => reset_alerting_token_project_settings_operations_path(project),
diff --git a/spec/helpers/organizations/organization_helper_spec.rb b/spec/helpers/organizations/organization_helper_spec.rb
index cf8ae358e49..9d55d2a84f8 100644
--- a/spec/helpers/organizations/organization_helper_spec.rb
+++ b/spec/helpers/organizations/organization_helper_spec.rb
@@ -91,4 +91,40 @@ RSpec.describe Organizations::OrganizationHelper, feature_category: :cell do
)
end
end
+
+ describe '#home_organization_setting_app_data' do
+ it 'returns expected json' do
+ expect(Gitlab::Json.parse(helper.home_organization_setting_app_data)).to eq(
+ {
+ 'initial_selection' => 1
+ }
+ )
+ end
+ end
+
+ describe '#organization_settings_general_app_data' do
+ it 'returns expected json' do
+ expect(Gitlab::Json.parse(helper.organization_settings_general_app_data(organization))).to eq(
+ {
+ 'organization' => {
+ 'id' => organization.id,
+ 'name' => organization.name,
+ 'path' => organization.path
+ },
+ 'organizations_path' => organizations_path,
+ 'root_url' => root_url
+ }
+ )
+ end
+ end
+
+ describe '#organization_user_app_data' do
+ it 'returns expected data object' do
+ expect(helper.organization_user_app_data(organization)).to eq(
+ {
+ organization_gid: organization.to_global_id
+ }
+ )
+ end
+ end
end
diff --git a/spec/helpers/preferences_helper_spec.rb b/spec/helpers/preferences_helper_spec.rb
index 9d1564dfef1..0bd8792ae83 100644
--- a/spec/helpers/preferences_helper_spec.rb
+++ b/spec/helpers/preferences_helper_spec.rb
@@ -71,6 +71,16 @@ RSpec.describe PreferencesHelper do
end
end
+ describe '#time_display_format_choices_with_default' do
+ it 'returns choices' do
+ expect(helper.time_display_format_choices).to eq({
+ "12-hour: 2:34 PM" => 1,
+ "24-hour: 14:34" => 2,
+ "System" => 0
+ })
+ end
+ end
+
describe '#user_application_theme' do
context 'with a user' do
it "returns user's theme's css_class" do
diff --git a/spec/helpers/projects/pipeline_helper_spec.rb b/spec/helpers/projects/pipeline_helper_spec.rb
index 16c9b8a85ec..7e117fe0cce 100644
--- a/spec/helpers/projects/pipeline_helper_spec.rb
+++ b/spec/helpers/projects/pipeline_helper_spec.rb
@@ -54,6 +54,7 @@ RSpec.describe Projects::PipelineHelper do
failure_reason: pipeline.failure_reason,
triggered_by_path: '',
schedule: pipeline.schedule?.to_s,
+ trigger: pipeline.trigger?.to_s,
child: pipeline.child?.to_s,
latest: pipeline.latest?.to_s,
merge_train_pipeline: pipeline.merge_train_pipeline?.to_s,
diff --git a/spec/helpers/projects_helper_spec.rb b/spec/helpers/projects_helper_spec.rb
index 90d998e17c3..1e05cf6a7ac 100644
--- a/spec/helpers/projects_helper_spec.rb
+++ b/spec/helpers/projects_helper_spec.rb
@@ -128,6 +128,24 @@ RSpec.describe ProjectsHelper, feature_category: :source_code_management do
end
end
+ describe '#load_catalog_resources' do
+ before_all do
+ create_list(:project, 2)
+ end
+
+ let_it_be(:projects) { Project.all.to_a }
+
+ it 'does not execute a database query when project.catalog_resource is accessed' do
+ helper.load_catalog_resources(projects)
+
+ queries = ActiveRecord::QueryRecorder.new do
+ projects.each(&:catalog_resource)
+ end
+
+ expect(queries).not_to exceed_query_limit(0)
+ end
+ end
+
describe '#last_pipeline_from_status_cache' do
before do
# clear cross-example caches
diff --git a/spec/helpers/search_helper_spec.rb b/spec/helpers/search_helper_spec.rb
index 1ff7e48abfc..e1c0aafc3c3 100644
--- a/spec/helpers/search_helper_spec.rb
+++ b/spec/helpers/search_helper_spec.rb
@@ -797,20 +797,24 @@ RSpec.describe SearchHelper, feature_category: :global_search do
allow(self).to receive(:current_user).and_return(:the_current_user)
end
- where(:input, :expected) do
- '0' | false
- '1' | true
- 'yes' | true
- 'no' | false
- 'true' | true
- 'false' | false
- true | true
- false | false
+ shared_context 'with inputs' do
+ where(:input, :expected) do
+ '0' | false
+ '1' | true
+ 'yes' | true
+ 'no' | false
+ 'true' | true
+ 'false' | false
+ true | true
+ false | false
+ end
end
describe 'for confidential' do
let(:params) { { confidential: input } }
+ include_context 'with inputs'
+
with_them do
it 'transforms param' do
expect(::SearchService).to receive(:new).with(:the_current_user, { confidential: expected })
@@ -823,6 +827,8 @@ RSpec.describe SearchHelper, feature_category: :global_search do
describe 'for include_archived' do
let(:params) { { include_archived: input } }
+ include_context 'with inputs'
+
with_them do
it 'transforms param' do
expect(::SearchService).to receive(:new).with(:the_current_user, { include_archived: expected })
diff --git a/spec/helpers/sidebars_helper_spec.rb b/spec/helpers/sidebars_helper_spec.rb
index 3e5ee714b32..c9131ca518f 100644
--- a/spec/helpers/sidebars_helper_spec.rb
+++ b/spec/helpers/sidebars_helper_spec.rb
@@ -287,6 +287,9 @@ RSpec.describe SidebarsHelper, feature_category: :navigation do
{ href: "/groups/new", text: "New group",
component: nil,
extraAttrs: extra_attrs.call("general_new_group") },
+ { href: "/-/organizations/new", text: s_('Organization|New organization'),
+ component: nil,
+ extraAttrs: extra_attrs.call("general_new_organization") },
{ href: "/-/snippets/new", text: "New snippet",
component: nil,
extraAttrs: extra_attrs.call("general_new_snippet") }
@@ -403,15 +406,15 @@ RSpec.describe SidebarsHelper, feature_category: :navigation do
describe 'context switcher persistent links' do
let_it_be(:public_link) do
- [
- { title: s_('Navigation|Explore'), link: '/explore', icon: 'compass' }
- ]
+ { title: s_('Navigation|Explore'), link: '/explore', icon: 'compass' }
end
let_it_be(:public_links_for_user) do
[
{ title: s_('Navigation|Your work'), link: '/', icon: 'work' },
- *public_link
+ public_link,
+ { title: s_('Navigation|Profile'), link: '/-/profile', icon: 'profile' },
+ { title: s_('Navigation|Preferences'), link: '/-/profile/preferences', icon: 'preferences' }
]
end
@@ -436,7 +439,7 @@ RSpec.describe SidebarsHelper, feature_category: :navigation do
let(:user) { nil }
it 'returns only the public links for an anonymous user' do
- expect(subject[:context_switcher_links]).to eq(public_link)
+ expect(subject[:context_switcher_links]).to eq([public_link])
end
end
diff --git a/spec/helpers/sorting_helper_spec.rb b/spec/helpers/sorting_helper_spec.rb
index 0f53cc98415..dccea889d55 100644
--- a/spec/helpers/sorting_helper_spec.rb
+++ b/spec/helpers/sorting_helper_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-RSpec.describe SortingHelper do
+RSpec.describe SortingHelper, feature_category: :shared do
include ApplicationHelper
include IconsHelper
include ExploreHelper
diff --git a/spec/helpers/users/callouts_helper_spec.rb b/spec/helpers/users/callouts_helper_spec.rb
index 10f021330db..5ec06507088 100644
--- a/spec/helpers/users/callouts_helper_spec.rb
+++ b/spec/helpers/users/callouts_helper_spec.rb
@@ -2,7 +2,7 @@
require "spec_helper"
-RSpec.describe Users::CalloutsHelper do
+RSpec.describe Users::CalloutsHelper, feature_category: :navigation do
let_it_be(:user, refind: true) { create(:user) }
before do
@@ -165,26 +165,6 @@ RSpec.describe Users::CalloutsHelper do
end
end
- describe '.show_pages_menu_callout?' do
- subject { helper.show_pages_menu_callout? }
-
- before do
- allow(helper).to receive(:user_dismissed?).with(described_class::PAGES_MOVED_CALLOUT) { dismissed }
- end
-
- context 'when user has not dismissed' do
- let(:dismissed) { false }
-
- it { is_expected.to be true }
- end
-
- context 'when user dismissed' do
- let(:dismissed) { true }
-
- it { is_expected.to be false }
- end
- end
-
describe '#web_hook_disabled_dismissed?', feature_category: :webhooks do
context 'without a project' do
it 'is false' do
diff --git a/spec/helpers/users_helper_spec.rb b/spec/helpers/users_helper_spec.rb
index 20b5452d2d4..a3d77d76591 100644
--- a/spec/helpers/users_helper_spec.rb
+++ b/spec/helpers/users_helper_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe UsersHelper do
+RSpec.describe UsersHelper, feature_category: :user_management do
include TermsHelper
let_it_be(:user) { create(:user, timezone: ActiveSupport::TimeZone::MAPPING['UTC']) }
diff --git a/spec/helpers/visibility_level_helper_spec.rb b/spec/helpers/visibility_level_helper_spec.rb
index 8f37bf29a4b..4af7fae400e 100644
--- a/spec/helpers/visibility_level_helper_spec.rb
+++ b/spec/helpers/visibility_level_helper_spec.rb
@@ -323,34 +323,4 @@ RSpec.describe VisibilityLevelHelper, feature_category: :system_access do
it { is_expected.to eq(expected) }
end
end
-
- describe '#visibility_level_options' do
- let(:user) { build(:user) }
-
- before do
- allow(helper).to receive(:current_user).and_return(user)
- end
-
- it 'returns the desired mapping' do
- expected_options = [
- {
- level: 0,
- label: 'Private',
- description: 'The group and its projects can only be viewed by members.'
- },
- {
- level: 10,
- label: 'Internal',
- description: 'The group and any internal projects can be viewed by any logged in user except external users.'
- },
- {
- level: 20,
- label: 'Public',
- description: 'The group and any public projects can be viewed without any authentication.'
- }
- ]
-
- expect(helper.visibility_level_options(group)).to eq expected_options
- end
- end
end
diff --git a/spec/helpers/vite_helper_spec.rb b/spec/helpers/vite_helper_spec.rb
deleted file mode 100644
index edb5650ab1a..00000000000
--- a/spec/helpers/vite_helper_spec.rb
+++ /dev/null
@@ -1,59 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe ViteHelper, feature_category: :tooling do
- let(:source) { 'foo.js' }
- let(:vite_source) { 'vite/foo.js' }
- let(:vite_tag) { '<tag src="vite/foo"></tag>' }
- let(:webpack_source) { 'webpack/foo.js' }
- let(:webpack_tag) { '<tag src="webpack/foo"></tag>' }
-
- context 'when vite enabled' do
- before do
- stub_rails_env('development')
- stub_feature_flags(vite: true)
-
- allow(helper).to receive(:vite_javascript_tag).and_return(vite_tag)
- allow(helper).to receive(:vite_asset_path).and_return(vite_source)
- allow(helper).to receive(:vite_stylesheet_tag).and_return(vite_tag)
- allow(helper).to receive(:vite_asset_url).and_return(vite_source)
- allow(helper).to receive(:vite_running).and_return(true)
- end
-
- describe '#universal_javascript_include_tag' do
- it 'returns vite javascript tag' do
- expect(helper.universal_javascript_include_tag(source)).to eq(vite_tag)
- end
- end
-
- describe '#universal_asset_path' do
- it 'returns vite asset path' do
- expect(helper.universal_asset_path(source)).to eq(vite_source)
- end
- end
- end
-
- context 'when vite disabled' do
- before do
- stub_feature_flags(vite: false)
-
- allow(helper).to receive(:javascript_include_tag).and_return(webpack_tag)
- allow(helper).to receive(:asset_path).and_return(webpack_source)
- allow(helper).to receive(:stylesheet_link_tag).and_return(webpack_tag)
- allow(helper).to receive(:path_to_stylesheet).and_return(webpack_source)
- end
-
- describe '#universal_javascript_include_tag' do
- it 'returns webpack javascript tag' do
- expect(helper.universal_javascript_include_tag(source)).to eq(webpack_tag)
- end
- end
-
- describe '#universal_asset_path' do
- it 'returns ActionView asset path' do
- expect(helper.universal_asset_path(source)).to eq(webpack_source)
- end
- end
- end
-end
diff --git a/spec/helpers/wiki_helper_spec.rb b/spec/helpers/wiki_helper_spec.rb
index 6eaa603a43d..31bbd394ac1 100644
--- a/spec/helpers/wiki_helper_spec.rb
+++ b/spec/helpers/wiki_helper_spec.rb
@@ -115,17 +115,6 @@ RSpec.describe WikiHelper, feature_category: :wiki do
end
end
- describe '#wiki_sort_title' do
- it 'returns a title corresponding to a key' do
- expect(helper.wiki_sort_title('created_at')).to eq('Created date')
- expect(helper.wiki_sort_title('title')).to eq('Title')
- end
-
- it 'defaults to Title if a key is unknown' do
- expect(helper.wiki_sort_title('unknown')).to eq('Title')
- end
- end
-
describe '#wiki_page_tracking_context' do
let_it_be(:page) { create(:wiki_page, title: 'path/to/page 💩', content: '💩', format: :markdown) }
diff --git a/spec/initializers/action_cable_subscription_adapter_identifier_spec.rb b/spec/initializers/action_cable_subscription_adapter_identifier_spec.rb
index dd2bf298611..cf82fd751dd 100644
--- a/spec/initializers/action_cable_subscription_adapter_identifier_spec.rb
+++ b/spec/initializers/action_cable_subscription_adapter_identifier_spec.rb
@@ -27,8 +27,7 @@ RSpec.describe 'ActionCableSubscriptionAdapterIdentifier override' do
sub = ActionCable.server.pubsub.send(:redis_connection)
- expect(sub.is_a?(::Gitlab::Redis::MultiStore)).to eq(true)
- expect(sub.secondary_store.connection[:id]).to eq('unix:///home/localuser/redis/redis.socket/0')
+ expect(sub.connection[:id]).to eq('unix:///home/localuser/redis/redis.socket/0')
expect(ActionCable.server.config.cable[:id]).to be_nil
end
end
diff --git a/spec/initializers/active_record_transaction_observer_spec.rb b/spec/initializers/active_record_transaction_observer_spec.rb
index a834037dce5..124bfe2eb48 100644
--- a/spec/initializers/active_record_transaction_observer_spec.rb
+++ b/spec/initializers/active_record_transaction_observer_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'ActiveRecord Transaction Observer', feature_category: :application_performance do
+RSpec.describe 'ActiveRecord Transaction Observer', feature_category: :cloud_connector do
def load_initializer
load Rails.root.join('config/initializers/active_record_transaction_observer.rb')
end
diff --git a/spec/initializers/diagnostic_reports_spec.rb b/spec/initializers/diagnostic_reports_spec.rb
index dc989efe809..ce184ecee29 100644
--- a/spec/initializers/diagnostic_reports_spec.rb
+++ b/spec/initializers/diagnostic_reports_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'diagnostic reports', :aggregate_failures, feature_category: :application_performance do
+RSpec.describe 'diagnostic reports', :aggregate_failures, feature_category: :cloud_connector do
subject(:load_initializer) do
load Rails.root.join('config/initializers/diagnostic_reports.rb')
end
diff --git a/spec/initializers/google_cloud_profiler_spec.rb b/spec/initializers/google_cloud_profiler_spec.rb
index 493d1e0bea5..d9b871fd1f0 100644
--- a/spec/initializers/google_cloud_profiler_spec.rb
+++ b/spec/initializers/google_cloud_profiler_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'google cloud profiler', :aggregate_failures, feature_category: :application_performance do
+RSpec.describe 'google cloud profiler', :aggregate_failures, feature_category: :cloud_connector do
subject(:load_initializer) do
load rails_root_join('config/initializers/google_cloud_profiler.rb')
end
diff --git a/spec/initializers/memory_watchdog_spec.rb b/spec/initializers/memory_watchdog_spec.rb
index ef24da0071b..d1dfb198818 100644
--- a/spec/initializers/memory_watchdog_spec.rb
+++ b/spec/initializers/memory_watchdog_spec.rb
@@ -2,7 +2,7 @@
require 'fast_spec_helper'
-RSpec.describe 'memory watchdog', feature_category: :application_performance do
+RSpec.describe 'memory watchdog', feature_category: :cloud_connector do
shared_examples 'starts configured watchdog' do |configure_monitor_method|
shared_examples 'configures and starts watchdog' do
it "correctly configures and starts watchdog", :aggregate_failures do
diff --git a/spec/lib/api/entities/bulk_imports/entity_failure_spec.rb b/spec/lib/api/entities/bulk_imports/entity_failure_spec.rb
index 0132102b117..217e6c11630 100644
--- a/spec/lib/api/entities/bulk_imports/entity_failure_spec.rb
+++ b/spec/lib/api/entities/bulk_imports/entity_failure_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::Entities::BulkImports::EntityFailure do
+RSpec.describe API::Entities::BulkImports::EntityFailure, feature_category: :importers do
let_it_be(:failure) { create(:bulk_import_failure) }
subject { described_class.new(failure).as_json }
@@ -10,11 +10,11 @@ RSpec.describe API::Entities::BulkImports::EntityFailure do
it 'has the correct attributes' do
expect(subject).to include(
:relation,
- :step,
- :exception_class,
:exception_message,
+ :exception_class,
:correlation_id_value,
- :created_at
+ :source_url,
+ :source_title
)
end
diff --git a/spec/lib/api/entities/wiki_page_spec.rb b/spec/lib/api/entities/wiki_page_spec.rb
index a3566293c5c..da75ade997b 100644
--- a/spec/lib/api/entities/wiki_page_spec.rb
+++ b/spec/lib/api/entities/wiki_page_spec.rb
@@ -24,7 +24,7 @@ RSpec.describe API::Entities::WikiPage do
context "with front matter content" do
let(:wiki_page) { create(:wiki_page) }
- let(:content_with_front_matter) { "---\nxxx: abc\n---\nHome Page" }
+ let(:content_with_front_matter) { "---\ntitle: abc\n---\nHome Page" }
before do
wiki_page.update(content: content_with_front_matter) # rubocop:disable Rails/SaveBang
@@ -33,6 +33,10 @@ RSpec.describe API::Entities::WikiPage do
it 'returns the raw wiki page content' do
expect(subject[:content]).to eq content_with_front_matter
end
+
+ it 'return the front matter title' do
+ expect(subject[:front_matter]).to eq({ title: "abc" })
+ end
end
context 'when render_html param is passed' do
diff --git a/spec/lib/api/github/entities_spec.rb b/spec/lib/api/github/entities_spec.rb
deleted file mode 100644
index 63c54b259a2..00000000000
--- a/spec/lib/api/github/entities_spec.rb
+++ /dev/null
@@ -1,31 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe API::Github::Entities do
- describe API::Github::Entities::User do
- let(:user) { create(:user, username: username) }
- let(:username) { 'name_of_user' }
- let(:gitlab_protocol_and_host) { "#{Gitlab.config.gitlab.protocol}://#{Gitlab.config.gitlab.host}" }
- let(:expected_user_url) { "#{gitlab_protocol_and_host}/#{username}" }
- let(:entity) { described_class.new(user) }
-
- subject { entity.as_json }
-
- specify :aggregate_failures do
- expect(subject[:id]).to eq user.id
- expect(subject[:login]).to eq 'name_of_user'
- expect(subject[:url]).to eq expected_user_url
- expect(subject[:html_url]).to eq expected_user_url
- expect(subject[:avatar_url]).to include('https://www.gravatar.com/avatar')
- end
-
- context 'with avatar' do
- let(:user) { create(:user, :with_avatar, username: username) }
-
- specify do
- expect(subject[:avatar_url]).to include("#{gitlab_protocol_and_host}/uploads/-/system/user/avatar/")
- end
- end
- end
-end
diff --git a/spec/lib/api/helpers/rate_limiter_spec.rb b/spec/lib/api/helpers/rate_limiter_spec.rb
index 531140a32a3..101b1c97184 100644
--- a/spec/lib/api/helpers/rate_limiter_spec.rb
+++ b/spec/lib/api/helpers/rate_limiter_spec.rb
@@ -69,4 +69,12 @@ RSpec.describe API::Helpers::RateLimiter do
end
end
end
+
+ describe '#mark_throttle!' do
+ it 'calls ApplicationRateLimiter#throttle?' do
+ expect(::Gitlab::ApplicationRateLimiter).to receive(:throttled?).with(key, scope: scope).and_return(false)
+
+ subject.mark_throttle!(key, scope: scope)
+ end
+ end
end
diff --git a/spec/lib/api/helpers_spec.rb b/spec/lib/api/helpers_spec.rb
index 5d343ec2777..21b3b8e6927 100644
--- a/spec/lib/api/helpers_spec.rb
+++ b/spec/lib/api/helpers_spec.rb
@@ -1327,4 +1327,79 @@ RSpec.describe API::Helpers, feature_category: :shared do
end
end
end
+
+ describe '#authenticate_by_gitlab_shell_or_workhorse_token!' do
+ include GitlabShellHelpers
+ include WorkhorseHelpers
+
+ include_context 'workhorse headers'
+
+ let(:headers) { {} }
+ let(:params) { {} }
+
+ context 'when request from gitlab shell' do
+ let(:valid_secret_token) { 'valid' }
+ let(:invalid_secret_token) { 'invalid' }
+
+ before do
+ allow(helper).to receive_messages(headers: headers)
+ end
+
+ context 'with invalid token' do
+ let(:headers) { gitlab_shell_internal_api_request_header(secret_token: invalid_secret_token) }
+
+ it 'unauthorized' do
+ expect(helper).to receive(:unauthorized!)
+
+ helper.authenticate_by_gitlab_shell_or_workhorse_token!
+ end
+ end
+
+ context 'with valid token' do
+ let(:headers) { gitlab_shell_internal_api_request_header }
+
+ it 'authorized' do
+ expect(helper).not_to receive(:unauthorized!)
+
+ helper.authenticate_by_gitlab_shell_or_workhorse_token!
+ end
+ end
+ end
+
+ context 'when request from gitlab workhorse' do
+ let(:env) { {} }
+ let(:request) { ActionDispatch::Request.new(env) }
+
+ before do
+ allow_any_instance_of(ActionDispatch::Request).to receive(:headers).and_return(headers)
+ allow(helper).to receive(:request).and_return(request)
+ allow(helper).to receive_messages(params: params, headers: headers, env: env)
+ end
+
+ context 'with invalid token' do
+ let(:headers) { { Gitlab::Workhorse::INTERNAL_API_REQUEST_HEADER => JWT.encode({ 'iss' => 'gitlab-workhorse' }, 'wrongkey', 'HS256') } }
+
+ before do
+ allow(JWT).to receive(:decode).and_return([{ 'iss' => 'gitlab-workhorse' }])
+ end
+
+ it 'unauthorized' do
+ expect(helper).to receive(:forbidden!)
+
+ helper.authenticate_by_gitlab_shell_or_workhorse_token!
+ end
+ end
+
+ context 'with valid token' do
+ let(:headers) { workhorse_headers }
+ let(:env) { { 'HTTP_GITLAB_WORKHORSE' => 1 } }
+
+ it 'authorized' do
+ expect(helper).not_to receive(:forbidden!)
+
+ helper.authenticate_by_gitlab_shell_or_workhorse_token!
+ end
+ end
+ end
+ end
end
diff --git a/spec/lib/atlassian/jira_connect/client_spec.rb b/spec/lib/atlassian/jira_connect/client_spec.rb
index f7597579e7a..a692d76da77 100644
--- a/spec/lib/atlassian/jira_connect/client_spec.rb
+++ b/spec/lib/atlassian/jira_connect/client_spec.rb
@@ -447,38 +447,77 @@ RSpec.describe Atlassian::JiraConnect::Client, feature_category: :integrations d
end
describe '#user_info' do
- let(:account_id) { '12345' }
- let(:response_body) do
- {
- groups: {
- items: [
- { name: 'site-admins' }
- ]
- }
- }.to_json
- end
+ context 'when user is a site administrator' do
+ let(:account_id) { '12345' }
+ let(:response_body) do
+ {
+ groups: {
+ items: [
+ { name: 'site-admins' }
+ ]
+ }
+ }.to_json
+ end
- before do
- stub_full_request("https://gitlab-test.atlassian.net/rest/api/3/user?accountId=#{account_id}&expand=groups")
- .to_return(status: response_status, body: response_body, headers: { 'Content-Type': 'application/json' })
- end
+ before do
+ stub_full_request("https://gitlab-test.atlassian.net/rest/api/3/user?accountId=#{account_id}&expand=groups")
+ .to_return(status: response_status, body: response_body, headers: { 'Content-Type': 'application/json' })
+ end
+
+ context 'with a successful response' do
+ let(:response_status) { 200 }
- context 'with a successful response' do
- let(:response_status) { 200 }
+ it 'returns a JiraUser instance' do
+ jira_user = client.user_info(account_id)
- it 'returns a JiraUser instance' do
- jira_user = client.user_info(account_id)
+ expect(jira_user).to be_a(Atlassian::JiraConnect::JiraUser)
+ expect(jira_user).to be_jira_admin
+ end
+ end
+
+ context 'with a failed response' do
+ let(:response_status) { 401 }
- expect(jira_user).to be_a(Atlassian::JiraConnect::JiraUser)
- expect(jira_user).to be_site_admin
+ it 'returns nil' do
+ expect(client.user_info(account_id)).to be_nil
+ end
end
end
- context 'with a failed response' do
- let(:response_status) { 401 }
+ context 'when user is an organization administrator' do
+ let(:account_id) { '12345' }
+ let(:response_body) do
+ {
+ groups: {
+ items: [
+ { name: 'org-admins' }
+ ]
+ }
+ }.to_json
+ end
+
+ before do
+ stub_full_request("https://gitlab-test.atlassian.net/rest/api/3/user?accountId=#{account_id}&expand=groups")
+ .to_return(status: response_status, body: response_body, headers: { 'Content-Type': 'application/json' })
+ end
+
+ context 'with a successful response' do
+ let(:response_status) { 200 }
+
+ it 'returns a JiraUser instance' do
+ jira_user = client.user_info(account_id)
+
+ expect(jira_user).to be_a(Atlassian::JiraConnect::JiraUser)
+ expect(jira_user).to be_jira_admin
+ end
+ end
+
+ context 'with a failed response' do
+ let(:response_status) { 401 }
- it 'returns nil' do
- expect(client.user_info(account_id)).to be_nil
+ it 'returns nil' do
+ expect(client.user_info(account_id)).to be_nil
+ end
end
end
end
diff --git a/spec/lib/backup/gitaly_backup_spec.rb b/spec/lib/backup/gitaly_backup_spec.rb
index 6c2656b1c48..058c7f12f63 100644
--- a/spec/lib/backup/gitaly_backup_spec.rb
+++ b/spec/lib/backup/gitaly_backup_spec.rb
@@ -166,25 +166,40 @@ RSpec.describe Backup::GitalyBackup, feature_category: :backup_restore do
let_it_be(:personal_snippet) { create(:personal_snippet, author: project.first_owner) }
let_it_be(:project_snippet) { create(:project_snippet, project: project, author: project.first_owner) }
- def copy_fixture_to_backup_path(backup_name, repo_disk_path)
- FileUtils.mkdir_p(File.join(Gitlab.config.backup.path, 'repositories', File.dirname(repo_disk_path)))
+ def create_repo_backup(backup_name, repo)
+ repo_backup_root = File.join(Gitlab.config.backup.path, 'repositories')
+
+ FileUtils.mkdir_p(File.join(repo_backup_root, 'manifests', repo.storage, repo.relative_path))
+ FileUtils.mkdir_p(File.join(repo_backup_root, repo.relative_path))
%w[.bundle .refs].each do |filetype|
FileUtils.cp(
Rails.root.join('spec/fixtures/lib/backup', backup_name + filetype),
- File.join(Gitlab.config.backup.path, 'repositories', repo_disk_path + filetype)
+ File.join(repo_backup_root, repo.relative_path + filetype)
)
end
+
+ manifest = <<-TOML
+ object_format = 'sha1'
+ head_references = 'heads/refs/master'
+
+ [[steps]]
+ bundle_path = '#{repo.relative_path}.bundle'
+ ref_path = '#{repo.relative_path}.refs'
+ custom_hooks_path = '#{repo.relative_path}.custom_hooks.tar'
+ TOML
+
+ File.write(File.join(repo_backup_root, 'manifests', repo.storage, repo.relative_path, backup_id + '.toml'), manifest)
end
it 'restores from repository bundles', :aggregate_failures do
- copy_fixture_to_backup_path('project_repo', project.disk_path)
- copy_fixture_to_backup_path('wiki_repo', project.wiki.disk_path)
- copy_fixture_to_backup_path('design_repo', project.design_repository.disk_path)
- copy_fixture_to_backup_path('personal_snippet_repo', personal_snippet.disk_path)
- copy_fixture_to_backup_path('project_snippet_repo', project_snippet.disk_path)
+ create_repo_backup('project_repo', project.repository.raw)
+ create_repo_backup('wiki_repo', project.wiki.repository)
+ create_repo_backup('design_repo', project.design_repository)
+ create_repo_backup('personal_snippet_repo', personal_snippet.repository)
+ create_repo_backup('project_snippet_repo', project_snippet.repository)
- expect(Open3).to receive(:popen2).with(expected_env, anything, 'restore', '-path', anything, '-layout', 'pointer').and_call_original
+ expect(Open3).to receive(:popen2).with(expected_env, anything, 'restore', '-path', anything, '-layout', 'pointer', '-id', backup_id).and_call_original
subject.start(:restore, destination, backup_id: backup_id)
subject.enqueue(project, Gitlab::GlRepository::PROJECT)
@@ -204,9 +219,9 @@ RSpec.describe Backup::GitalyBackup, feature_category: :backup_restore do
end
it 'clears specified storages when remove_all_repositories is set' do
- expect(Open3).to receive(:popen2).with(expected_env, anything, 'restore', '-path', anything, '-layout', 'pointer', '-remove-all-repositories', 'default').and_call_original
+ expect(Open3).to receive(:popen2).with(expected_env, anything, 'restore', '-path', anything, '-layout', 'pointer', '-remove-all-repositories', 'default', '-id', backup_id).and_call_original
- copy_fixture_to_backup_path('project_repo', project.disk_path)
+ create_repo_backup('project_repo', project.repository.raw)
subject.start(:restore, destination, backup_id: backup_id, remove_all_repositories: %w[default])
subject.enqueue(project, Gitlab::GlRepository::PROJECT)
subject.finish!
@@ -216,7 +231,7 @@ RSpec.describe Backup::GitalyBackup, feature_category: :backup_restore do
let(:max_parallelism) { 3 }
it 'passes parallel option through' do
- expect(Open3).to receive(:popen2).with(expected_env, anything, 'restore', '-path', anything, '-layout', 'pointer', '-parallel', '3').and_call_original
+ expect(Open3).to receive(:popen2).with(expected_env, anything, 'restore', '-path', anything, '-layout', 'pointer', '-parallel', '3', '-id', backup_id).and_call_original
subject.start(:restore, destination, backup_id: backup_id)
subject.finish!
@@ -227,7 +242,7 @@ RSpec.describe Backup::GitalyBackup, feature_category: :backup_restore do
let(:storage_parallelism) { 3 }
it 'passes parallel option through' do
- expect(Open3).to receive(:popen2).with(expected_env, anything, 'restore', '-path', anything, '-layout', 'pointer', '-parallel-storage', '3').and_call_original
+ expect(Open3).to receive(:popen2).with(expected_env, anything, 'restore', '-path', anything, '-layout', 'pointer', '-parallel-storage', '3', '-id', backup_id).and_call_original
subject.start(:restore, destination, backup_id: backup_id)
subject.finish!
diff --git a/spec/lib/banzai/filter/asset_proxy_filter_spec.rb b/spec/lib/banzai/filter/asset_proxy_filter_spec.rb
index baa22e08971..cb696e21e65 100644
--- a/spec/lib/banzai/filter/asset_proxy_filter_spec.rb
+++ b/spec/lib/banzai/filter/asset_proxy_filter_spec.rb
@@ -62,6 +62,8 @@ RSpec.describe Banzai::Filter::AssetProxyFilter, feature_category: :team_plannin
end
context 'when properly configured' do
+ using RSpec::Parameterized::TableSyntax
+
before do
stub_asset_proxy_setting(enabled: true)
stub_asset_proxy_setting(secret_key: 'shared-secret')
@@ -71,84 +73,33 @@ RSpec.describe Banzai::Filter::AssetProxyFilter, feature_category: :team_plannin
@context = described_class.transform_context({})
end
- it 'replaces img src' do
- src = 'http://example.com/test.png'
- new_src = 'https://assets.example.com/08df250eeeef1a8cf2c761475ac74c5065105612/687474703a2f2f6578616d706c652e636f6d2f746573742e706e67'
- doc = filter(image(src), @context)
-
- expect(doc.at_css('img')['src']).to eq new_src
- expect(doc.at_css('img')['data-canonical-src']).to eq src
- end
-
- it 'replaces invalid URLs' do
- src = '///example.com/test.png'
- new_src = 'https://assets.example.com/3368d2c7b9bed775bdd1e811f36a4b80a0dcd8ab/2f2f2f6578616d706c652e636f6d2f746573742e706e67'
- doc = filter(image(src), @context)
-
- expect(doc.at_css('img')['src']).to eq new_src
- expect(doc.at_css('img')['data-canonical-src']).to eq src
- end
-
- it 'replaces by default, even strings that do not look like URLs' do
- src = 'oigjsie8787%$**(#(%0'
- new_src = 'https://assets.example.com/1b893f9a71d66c99437f27e19b9a061a6f5d9391/6f69676a7369653837383725242a2a2823282530'
- doc = filter(image(src), @context)
-
- expect(doc.at_css('img')['src']).to eq new_src
- expect(doc.at_css('img')['data-canonical-src']).to eq src
- end
-
- it 'replaces URL with non-ASCII characters' do
- src = 'https://example.com/x?¬'
- new_src = 'https://assets.example.com/2f29a8c7f13f3ae14dc18c154dbbd657d703e75f/68747470733a2f2f6578616d706c652e636f6d2f783fc2ac'
- doc = filter(image(src), @context)
-
- expect(doc.at_css('img')['src']).to eq new_src
- expect(doc.at_css('img')['data-canonical-src']).to eq src
- end
-
- it 'replaces out-of-spec URL that would still be rendered in the browser' do
- src = 'https://example.com/##'
- new_src = 'https://assets.example.com/d7d0c845cc553d9430804c07e9456545ef3e6fe6/68747470733a2f2f6578616d706c652e636f6d2f2323'
- doc = filter(image(src), @context)
-
- expect(doc.at_css('img')['src']).to eq new_src
- expect(doc.at_css('img')['data-canonical-src']).to eq src
+ where(:data_canonical_src, :src) do
+ 'http://example.com/test.png' | 'https://assets.example.com/08df250eeeef1a8cf2c761475ac74c5065105612/687474703a2f2f6578616d706c652e636f6d2f746573742e706e67'
+ '///example.com/test.png' | 'https://assets.example.com/3368d2c7b9bed775bdd1e811f36a4b80a0dcd8ab/2f2f2f6578616d706c652e636f6d2f746573742e706e67'
+ '//example.com/test.png' | 'https://assets.example.com/a2e9aa56319e31bbd05be72e633f2864ff08becb/2f2f6578616d706c652e636f6d2f746573742e706e67'
+ # If it can't be parsed, default to use asset proxy
+ 'oigjsie8787%$**(#(%0' | 'https://assets.example.com/1b893f9a71d66c99437f27e19b9a061a6f5d9391/6f69676a7369653837383725242a2a2823282530'
+ 'https://example.com/x?¬' | 'https://assets.example.com/2f29a8c7f13f3ae14dc18c154dbbd657d703e75f/68747470733a2f2f6578616d706c652e636f6d2f783fc2ac'
+ # The browser loads this as if it was a valid URL
+ 'http:example.com' | 'https://assets.example.com/bcefecd18484ec2850887d6730273e5e70f5ed1a/687474703a6578616d706c652e636f6d'
+ 'https:example.com' | 'https://assets.example.com/648e074361143780357db0b5cf73d4438d5484d3/68747470733a6578616d706c652e636f6d'
+ 'https://example.com/##' | 'https://assets.example.com/d7d0c845cc553d9430804c07e9456545ef3e6fe6/68747470733a2f2f6578616d706c652e636f6d2f2323'
+ nil | "test.png"
+ nil | "/test.png"
+ nil | "#{Gitlab.config.gitlab.url}/test.png"
+ nil | 'http://gitlab.com/test.png'
+ nil | 'http://gitlab.com/test.png?url=http://example.com/test.png'
+ nil | 'http://images.mydomain.com/test.png'
end
- it 'skips internal images' do
- src = "#{Gitlab.config.gitlab.url}/test.png"
- doc = filter(image(src), @context)
+ with_them do
+ it 'correctly modifies the img tag' do
+ original_url = data_canonical_src || src
+ doc = filter(image(original_url), @context)
- expect(doc.at_css('img')['src']).to eq src
- end
-
- it 'skip relative urls' do
- src = "/test.png"
- doc = filter(image(src), @context)
-
- expect(doc.at_css('img')['src']).to eq src
- end
-
- it 'skips single domain' do
- src = "http://gitlab.com/test.png"
- doc = filter(image(src), @context)
-
- expect(doc.at_css('img')['src']).to eq src
- end
-
- it 'skips single domain and ignores url in query string' do
- src = "http://gitlab.com/test.png?url=http://example.com/test.png"
- doc = filter(image(src), @context)
-
- expect(doc.at_css('img')['src']).to eq src
- end
-
- it 'skips wildcarded domain' do
- src = "http://images.mydomain.com/test.png"
- doc = filter(image(src), @context)
-
- expect(doc.at_css('img')['src']).to eq src
+ expect(doc.at_css('img')['src']).to eq src
+ expect(doc.at_css('img')['data-canonical-src']).to eq data_canonical_src
+ end
end
end
end
diff --git a/spec/lib/banzai/filter/math_filter_spec.rb b/spec/lib/banzai/filter/math_filter_spec.rb
index 3fa0f9028e8..9e9e4110b44 100644
--- a/spec/lib/banzai/filter/math_filter_spec.rb
+++ b/spec/lib/banzai/filter/math_filter_spec.rb
@@ -210,18 +210,31 @@ RSpec.describe Banzai::Filter::MathFilter, feature_category: :team_planning do
context 'when limiting how many elements can be marked as math' do
subject { pipeline_filter('$`2+2`$ + $3+3$ + $$4+4$$') }
- it 'enforces limits by default' do
+ before do
stub_const('Banzai::Filter::MathFilter::RENDER_NODES_LIMIT', 2)
+ end
+ it 'enforces limits by default' do
expect(subject.search('.js-render-math').count).to eq(2)
end
it 'does not limit when math_rendering_limits_enabled is false' do
stub_application_setting(math_rendering_limits_enabled: false)
- stub_const('Banzai::Filter::MathFilter::RENDER_NODES_LIMIT', 2)
expect(subject.search('.js-render-math').count).to eq(3)
end
+
+ it 'does not limit for the wiki' do
+ doc = pipeline_filter('$`2+2`$ + $3+3$ + $$4+4$$', { wiki: true })
+
+ expect(doc.search('.js-render-math').count).to eq(3)
+ end
+
+ it 'does not limit for blobs' do
+ doc = pipeline_filter('$`2+2`$ + $3+3$ + $$4+4$$', { text_source: :blob })
+
+ expect(doc.search('.js-render-math').count).to eq(3)
+ end
end
it 'protects against malicious backtracking' do
@@ -232,14 +245,14 @@ RSpec.describe Banzai::Filter::MathFilter, feature_category: :team_planning do
end.not_to raise_error
end
- def pipeline_filter(text)
- context = { project: nil, no_sourcepos: true }
+ def pipeline_filter(text, context = {})
+ context = { project: nil, no_sourcepos: true }.merge(context)
doc = Banzai::Pipeline::PreProcessPipeline.call(text, {})
doc = Banzai::Pipeline::PlainMarkdownPipeline.call(doc[:output], context)
doc = Banzai::Filter::CodeLanguageFilter.call(doc[:output], context, nil)
doc = Banzai::Filter::SanitizationFilter.call(doc, context, nil)
- filter(doc)
+ filter(doc, context)
end
end
diff --git a/spec/lib/banzai/filter/references/user_reference_filter_spec.rb b/spec/lib/banzai/filter/references/user_reference_filter_spec.rb
index 7a11ff3ac3d..b4f9d1a22cf 100644
--- a/spec/lib/banzai/filter/references/user_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/references/user_reference_filter_spec.rb
@@ -231,16 +231,17 @@ RSpec.describe Banzai::Filter::References::UserReferenceFilter, feature_category
it 'does not have N+1 per multiple user references', :use_sql_query_cache do
markdown = reference.to_s
+ reference_filter(markdown) # warm up
control_count = ActiveRecord::QueryRecorder.new(skip_cached: false) do
reference_filter(markdown)
- end.count
+ end
markdown = "#{reference} @qwertyuiopzx @wertyuio @ertyu @rtyui #{reference2} #{reference3}"
expect do
reference_filter(markdown)
- end.not_to exceed_all_query_limit(control_count)
+ end.to issue_same_number_of_queries_as(control_count)
end
end
end
diff --git a/spec/lib/banzai/reference_parser/user_parser_spec.rb b/spec/lib/banzai/reference_parser/user_parser_spec.rb
index 179e6e73fa3..096f1d3792b 100644
--- a/spec/lib/banzai/reference_parser/user_parser_spec.rb
+++ b/spec/lib/banzai/reference_parser/user_parser_spec.rb
@@ -25,8 +25,14 @@ RSpec.describe Banzai::ReferenceParser::UserParser, feature_category: :team_plan
context 'when group has members' do
let!(:group_member) { create(:group_member, group: group, user: user) }
-
- it 'returns the users of the group' do
+ let!(:user2) { create(:user) }
+ let!(:user3) { create(:user) }
+ let!(:user4) { create(:user) }
+ let!(:group_member2) { create(:group_member, :minimal_access, group: group, user: user2) }
+ let!(:group_member3) { create(:group_member, :access_request, group: group, user: user3) }
+ let!(:group_member4) { create(:group_member, :invited, group: group, user: user4) }
+
+ it 'returns the relevant users of the group with enough access' do
expect(subject.referenced_by([link])).to eq([user])
end
diff --git a/spec/lib/bitbucket/representation/pull_request_comment_spec.rb b/spec/lib/bitbucket/representation/pull_request_comment_spec.rb
index e748cd7b955..6fdd1dfa561 100644
--- a/spec/lib/bitbucket/representation/pull_request_comment_spec.rb
+++ b/spec/lib/bitbucket/representation/pull_request_comment_spec.rb
@@ -2,7 +2,7 @@
require 'fast_spec_helper'
-RSpec.describe Bitbucket::Representation::PullRequestComment do
+RSpec.describe Bitbucket::Representation::PullRequestComment, feature_category: :importers do
describe '#iid' do
it { expect(described_class.new('id' => 1).iid).to eq(1) }
end
@@ -33,4 +33,10 @@ RSpec.describe Bitbucket::Representation::PullRequestComment do
it { expect(described_class.new('parent' => {}).has_parent?).to be_truthy }
it { expect(described_class.new({}).has_parent?).to be_falsey }
end
+
+ describe '#deleted?' do
+ it { expect(described_class.new('deleted' => true).deleted?).to be_truthy }
+ it { expect(described_class.new('deleted' => false).deleted?).to be_falsey }
+ it { expect(described_class.new({}).deleted?).to be_falsey }
+ end
end
diff --git a/spec/lib/bitbucket/representation/repo_spec.rb b/spec/lib/bitbucket/representation/repo_spec.rb
index ba5a3306d07..ac6095dedd1 100644
--- a/spec/lib/bitbucket/representation/repo_spec.rb
+++ b/spec/lib/bitbucket/representation/repo_spec.rb
@@ -2,7 +2,7 @@
require 'fast_spec_helper'
-RSpec.describe Bitbucket::Representation::Repo do
+RSpec.describe Bitbucket::Representation::Repo, feature_category: :importers do
describe '#has_wiki?' do
it { expect(described_class.new({ 'has_wiki' => false }).has_wiki?).to be_falsey }
it { expect(described_class.new({ 'has_wiki' => true }).has_wiki?).to be_truthy }
@@ -42,6 +42,11 @@ RSpec.describe Bitbucket::Representation::Repo do
it { expect(described_class.new({ 'full_name' => 'ben/test' }).slug).to eq('test') }
end
+ describe '#default_branch' do
+ it { expect(described_class.new({ 'mainbranch' => { 'name' => 'master' } }).default_branch).to eq('master') }
+ it { expect(described_class.new({}).default_branch).to eq(nil) }
+ end
+
describe '#clone_url' do
it 'builds url' do
data = { 'links' => { 'clone' => [{ 'name' => 'https', 'href' => 'https://bibucket.org/test/test.git' }] } }
diff --git a/spec/lib/bulk_imports/clients/graphql_spec.rb b/spec/lib/bulk_imports/clients/graphql_spec.rb
index 9bb37a7c438..16f98ed462e 100644
--- a/spec/lib/bulk_imports/clients/graphql_spec.rb
+++ b/spec/lib/bulk_imports/clients/graphql_spec.rb
@@ -8,13 +8,13 @@ RSpec.describe BulkImports::Clients::Graphql, feature_category: :importers do
subject { described_class.new(url: config.url, token: config.access_token) }
describe '#execute' do
- let(:graphql_client_double) { double }
let(:response_double) { double }
describe 'network errors' do
before do
allow(Gitlab::HTTP)
.to receive(:post)
+ .with(an_instance_of(String), a_hash_including(timeout: 60))
.and_return(response_double)
end
diff --git a/spec/lib/bulk_imports/common/pipelines/badges_pipeline_spec.rb b/spec/lib/bulk_imports/common/pipelines/badges_pipeline_spec.rb
index a18d26bedf3..aeb48bed314 100644
--- a/spec/lib/bulk_imports/common/pipelines/badges_pipeline_spec.rb
+++ b/spec/lib/bulk_imports/common/pipelines/badges_pipeline_spec.rb
@@ -39,18 +39,6 @@ RSpec.describe BulkImports::Common::Pipelines::BadgesPipeline do
expect { pipeline.run }.to not_change(Badge, :count)
end
- context 'with FF bulk_import_idempotent_workers disabled' do
- before do
- stub_feature_flags(bulk_import_idempotent_workers: false)
- end
-
- it 'creates duplicated badges' do
- expect { pipeline.run }.to change(Badge, :count).by(2)
-
- expect { pipeline.run }.to change(Badge, :count)
- end
- end
-
context 'when project entity' do
let(:first_page) { extracted_data(has_next_page: true) }
let(:last_page) { extracted_data(name: 'badge2', kind: 'project') }
diff --git a/spec/lib/bulk_imports/common/pipelines/entity_finisher_spec.rb b/spec/lib/bulk_imports/common/pipelines/entity_finisher_spec.rb
index 8ca74565788..b96ea20c676 100644
--- a/spec/lib/bulk_imports/common/pipelines/entity_finisher_spec.rb
+++ b/spec/lib/bulk_imports/common/pipelines/entity_finisher_spec.rb
@@ -9,7 +9,7 @@ RSpec.describe BulkImports::Common::Pipelines::EntityFinisher, feature_category:
context = BulkImports::Pipeline::Context.new(pipeline_tracker)
subject = described_class.new(context)
- expect_next_instance_of(Gitlab::Import::Logger) do |logger|
+ expect_next_instance_of(BulkImports::Logger) do |logger|
expect(logger)
.to receive(:info)
.with(
@@ -19,8 +19,7 @@ RSpec.describe BulkImports::Common::Pipelines::EntityFinisher, feature_category:
source_full_path: entity.source_full_path,
pipeline_class: described_class.name,
message: 'Entity finished',
- source_version: entity.bulk_import.source_version_info.to_s,
- importer: 'gitlab_migration'
+ source_version: entity.bulk_import.source_version_info.to_s
)
end
diff --git a/spec/lib/bulk_imports/pipeline/runner_spec.rb b/spec/lib/bulk_imports/pipeline/runner_spec.rb
index 8d48606633a..4540408990c 100644
--- a/spec/lib/bulk_imports/pipeline/runner_spec.rb
+++ b/spec/lib/bulk_imports/pipeline/runner_spec.rb
@@ -43,7 +43,9 @@ RSpec.describe BulkImports::Pipeline::Runner, feature_category: :importers do
stub_const('BulkImports::MyPipeline', pipeline)
end
- let_it_be_with_reload(:entity) { create(:bulk_import_entity) }
+ let_it_be(:bulk_import) { create(:bulk_import) }
+ let_it_be(:configuration) { create(:bulk_import_configuration, bulk_import: bulk_import) }
+ let_it_be_with_reload(:entity) { create(:bulk_import_entity, bulk_import: bulk_import) }
let(:tracker) { create(:bulk_import_tracker, entity: entity) }
let(:context) { BulkImports::Pipeline::Context.new(tracker, extra: :data) }
@@ -52,7 +54,7 @@ RSpec.describe BulkImports::Pipeline::Runner, feature_category: :importers do
shared_examples 'failed pipeline' do |exception_class, exception_message|
it 'logs import failure' do
- expect_next_instance_of(Gitlab::Import::Logger) do |logger|
+ expect_next_instance_of(BulkImports::Logger) do |logger|
expect(logger).to receive(:error)
.with(
a_hash_including(
@@ -67,7 +69,6 @@ RSpec.describe BulkImports::Pipeline::Runner, feature_category: :importers do
'correlation_id' => anything,
'class' => 'BulkImports::MyPipeline',
'message' => 'An object of a pipeline failed to import',
- 'importer' => 'gitlab_migration',
'exception.backtrace' => anything,
'source_version' => entity.bulk_import.source_version_info.to_s
)
@@ -92,14 +93,13 @@ RSpec.describe BulkImports::Pipeline::Runner, feature_category: :importers do
end
it 'logs a warn message and marks entity and tracker as failed' do
- expect_next_instance_of(Gitlab::Import::Logger) do |logger|
+ expect_next_instance_of(BulkImports::Logger) do |logger|
expect(logger).to receive(:warn)
.with(
log_params(
context,
message: 'Aborting entity migration due to pipeline failure',
- pipeline_class: 'BulkImports::MyPipeline',
- importer: 'gitlab_migration'
+ pipeline_class: 'BulkImports::MyPipeline'
)
)
end
@@ -119,6 +119,56 @@ RSpec.describe BulkImports::Pipeline::Runner, feature_category: :importers do
expect(entity.failed?).to eq(false)
end
end
+
+ context 'when failure happens during loader' do
+ before do
+ allow(tracker).to receive(:pipeline_class).and_return(BulkImports::MyPipeline)
+ allow(BulkImports::MyPipeline).to receive(:relation).and_return(relation)
+
+ allow_next_instance_of(BulkImports::Extractor) do |extractor|
+ allow(extractor).to receive(:extract).with(context).and_return(extracted_data)
+ end
+
+ allow_next_instance_of(BulkImports::Transformer) do |transformer|
+ allow(transformer).to receive(:transform).with(context, extracted_data.data.first).and_return(entry)
+ end
+
+ allow_next_instance_of(BulkImports::Loader) do |loader|
+ allow(loader).to receive(:load).with(context, entry).and_raise(StandardError, 'Error!')
+ end
+ end
+
+ context 'when entry has title' do
+ let(:relation) { 'issues' }
+ let(:entry) { Issue.new(iid: 1, title: 'hello world') }
+
+ it 'creates failure record with source url and title' do
+ subject.run
+
+ failure = entity.failures.first
+ expected_source_url = File.join(configuration.url, 'groups', entity.source_full_path, '-', 'issues', '1')
+
+ expect(failure).to be_present
+ expect(failure.source_url).to eq(expected_source_url)
+ expect(failure.source_title).to eq('hello world')
+ end
+ end
+
+ context 'when entry has name' do
+ let(:relation) { 'boards' }
+ let(:entry) { Board.new(name: 'hello world') }
+
+ it 'creates failure record with name' do
+ subject.run
+
+ failure = entity.failures.first
+
+ expect(failure).to be_present
+ expect(failure.source_url).to be_nil
+ expect(failure.source_title).to eq('hello world')
+ end
+ end
+ end
end
describe 'pipeline runner' do
@@ -144,6 +194,8 @@ RSpec.describe BulkImports::Pipeline::Runner, feature_category: :importers do
.with(context, extracted_data.data.first)
end
+ expect(subject).to receive(:on_finish)
+
expect_next_instance_of(Gitlab::Import::Logger) do |logger|
expect(logger).to receive(:info)
.with(
@@ -185,6 +237,14 @@ RSpec.describe BulkImports::Pipeline::Runner, feature_category: :importers do
log_params(
context,
pipeline_class: 'BulkImports::MyPipeline',
+ pipeline_step: :on_finish
+ )
+ )
+ expect(logger).to receive(:info)
+ .with(
+ log_params(
+ context,
+ pipeline_class: 'BulkImports::MyPipeline',
pipeline_step: :after_run
)
)
@@ -201,6 +261,28 @@ RSpec.describe BulkImports::Pipeline::Runner, feature_category: :importers do
subject.run
end
+ context 'when the pipeline is batched' do
+ let(:tracker) { create(:bulk_import_tracker, :batched, entity: entity) }
+
+ before do
+ allow_next_instance_of(BulkImports::Extractor) do |extractor|
+ allow(extractor).to receive(:extract).and_return(extracted_data)
+ end
+ end
+
+ it 'calls after_run' do
+ expect(subject).to receive(:after_run)
+
+ subject.run
+ end
+
+ it 'does not call on_finish' do
+ expect(subject).not_to receive(:on_finish)
+
+ subject.run
+ end
+ end
+
context 'when extracted data has multiple pages' do
it 'updates tracker information and runs pipeline again' do
first_page = extracted_data(has_next_page: true)
@@ -299,34 +381,6 @@ RSpec.describe BulkImports::Pipeline::Runner, feature_category: :importers do
subject.run
end
-
- context 'with FF bulk_import_idempotent_workers disabled' do
- before do
- stub_feature_flags(bulk_import_idempotent_workers: false)
- end
-
- it 'does not touch the cache' do
- expect_next_instance_of(BulkImports::Extractor) do |extractor|
- expect(extractor)
- .to receive(:extract)
- .with(context)
- .and_return(extracted_data)
- end
-
- expect_next_instance_of(BulkImports::Transformer) do |transformer|
- expect(transformer)
- .to receive(:transform)
- .with(context, extracted_data.data.first)
- .and_return(extracted_data.data.first)
- end
-
- expect_next_instance_of(BulkImports::MyPipeline) do |klass|
- expect(klass).not_to receive(:save_processed_entry)
- end
-
- subject.run
- end
- end
end
context 'when the entry is already processed' do
@@ -356,43 +410,13 @@ RSpec.describe BulkImports::Pipeline::Runner, feature_category: :importers do
subject.run
end
-
- context 'with FF bulk_import_idempotent_workers disabled' do
- before do
- stub_feature_flags(bulk_import_idempotent_workers: false)
- end
-
- it 'calls extractor, transformer, and loader' do
- expect_next_instance_of(BulkImports::Extractor) do |extractor|
- expect(extractor)
- .to receive(:extract)
- .with(context)
- .and_return(extracted_data)
- end
-
- expect_next_instance_of(BulkImports::Transformer) do |transformer|
- expect(transformer)
- .to receive(:transform)
- .with(context, extracted_data.data.first)
- .and_return(extracted_data.data.first)
- end
-
- expect_next_instance_of(BulkImports::Loader) do |loader|
- expect(loader)
- .to receive(:load)
- .with(context, extracted_data.data.first)
- end
-
- subject.run
- end
- end
end
context 'when entity is marked as failed' do
it 'logs and returns without execution' do
entity.fail_op!
- expect_next_instance_of(Gitlab::Import::Logger) do |logger|
+ expect_next_instance_of(BulkImports::Logger) do |logger|
expect(logger).to receive(:warn)
.with(
log_params(
@@ -414,14 +438,17 @@ RSpec.describe BulkImports::Pipeline::Runner, feature_category: :importers do
bulk_import_entity_type: context.entity.source_type,
source_full_path: entity.source_full_path,
source_version: context.entity.bulk_import.source_version_info.to_s,
- importer: 'gitlab_migration',
context_extra: context.extra
}.merge(extra)
end
def extracted_data(has_next_page: false)
BulkImports::Pipeline::ExtractedData.new(
- data: { foo: :bar },
+ data: {
+ 'foo' => 'bar',
+ 'title' => 'hello world',
+ 'iid' => 1
+ },
page_info: {
'has_next_page' => has_next_page,
'next_page' => has_next_page ? 'cursor' : nil
diff --git a/spec/lib/bulk_imports/pipeline_schema_info_spec.rb b/spec/lib/bulk_imports/pipeline_schema_info_spec.rb
new file mode 100644
index 00000000000..45dd92ca26d
--- /dev/null
+++ b/spec/lib/bulk_imports/pipeline_schema_info_spec.rb
@@ -0,0 +1,60 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe BulkImports::PipelineSchemaInfo, feature_category: :importers do
+ let(:entity) { build(:bulk_import_entity, :project_entity) }
+ let(:tracker) { build(:bulk_import_tracker, entity: entity, pipeline_name: pipeline_name) }
+
+ let(:pipeline_name) { BulkImports::Common::Pipelines::LabelsPipeline.to_s }
+
+ subject { described_class.new(tracker.pipeline_class, tracker.entity.portable_class) }
+
+ describe '#db_schema' do
+ context 'when pipeline defines a relation name which is an association' do
+ it 'returns the schema name of the table used by the association' do
+ expect(subject.db_schema).to eq(:gitlab_main_cell)
+ end
+ end
+
+ context 'when pipeline does not define a relation name' do
+ let(:pipeline_name) { BulkImports::Common::Pipelines::EntityFinisher.to_s }
+
+ it 'returns nil' do
+ expect(subject.db_schema).to eq(nil)
+ end
+ end
+
+ context 'when pipeline relation name is not an association' do
+ let(:pipeline_name) { BulkImports::Projects::Pipelines::CommitNotesPipeline.to_s }
+
+ it 'returns nil' do
+ expect(subject.db_schema).to eq(nil)
+ end
+ end
+ end
+
+ describe '#db_table' do
+ context 'when pipeline defines a relation name which is an association' do
+ it 'returns the name of the table used by the association' do
+ expect(subject.db_table).to eq('labels')
+ end
+ end
+
+ context 'when pipeline does not define a relation name' do
+ let(:pipeline_name) { BulkImports::Common::Pipelines::EntityFinisher.to_s }
+
+ it 'returns nil' do
+ expect(subject.db_table).to eq(nil)
+ end
+ end
+
+ context 'when pipeline relation name is not an association' do
+ let(:pipeline_name) { BulkImports::Projects::Pipelines::CommitNotesPipeline.to_s }
+
+ it 'returns nil' do
+ expect(subject.db_table).to eq(nil)
+ end
+ end
+ end
+end
diff --git a/spec/lib/bulk_imports/projects/pipelines/container_expiration_policy_pipeline_spec.rb b/spec/lib/bulk_imports/projects/pipelines/container_expiration_policy_pipeline_spec.rb
index 9dac8e45ef9..334c2004b59 100644
--- a/spec/lib/bulk_imports/projects/pipelines/container_expiration_policy_pipeline_spec.rb
+++ b/spec/lib/bulk_imports/projects/pipelines/container_expiration_policy_pipeline_spec.rb
@@ -2,10 +2,10 @@
require 'spec_helper'
-RSpec.describe BulkImports::Projects::Pipelines::ContainerExpirationPolicyPipeline do
+RSpec.describe BulkImports::Projects::Pipelines::ContainerExpirationPolicyPipeline, feature_category: :importers do
let_it_be(:project) { create(:project) }
let_it_be(:entity) { create(:bulk_import_entity, :project_entity, project: project) }
- let_it_be(:tracker) { create(:bulk_import_tracker, entity: entity) }
+ let_it_be(:tracker) { create(:bulk_import_tracker, entity: entity, pipeline_name: described_class) }
let_it_be(:context) { BulkImports::Pipeline::Context.new(tracker) }
let_it_be(:policy) do
diff --git a/spec/lib/bulk_imports/projects/pipelines/external_pull_requests_pipeline_spec.rb b/spec/lib/bulk_imports/projects/pipelines/external_pull_requests_pipeline_spec.rb
index b7197814f9c..f00da47d9f5 100644
--- a/spec/lib/bulk_imports/projects/pipelines/external_pull_requests_pipeline_spec.rb
+++ b/spec/lib/bulk_imports/projects/pipelines/external_pull_requests_pipeline_spec.rb
@@ -2,11 +2,11 @@
require 'spec_helper'
-RSpec.describe BulkImports::Projects::Pipelines::ExternalPullRequestsPipeline do
+RSpec.describe BulkImports::Projects::Pipelines::ExternalPullRequestsPipeline, feature_category: :importers do
let_it_be(:project) { create(:project) }
let_it_be(:bulk_import) { create(:bulk_import) }
let_it_be(:entity) { create(:bulk_import_entity, :project_entity, project: project, bulk_import: bulk_import) }
- let_it_be(:tracker) { create(:bulk_import_tracker, entity: entity) }
+ let_it_be(:tracker) { create(:bulk_import_tracker, entity: entity, pipeline_name: described_class) }
let_it_be(:context) { BulkImports::Pipeline::Context.new(tracker) }
let(:attributes) { {} }
diff --git a/spec/lib/bulk_imports/projects/pipelines/merge_requests_pipeline_spec.rb b/spec/lib/bulk_imports/projects/pipelines/merge_requests_pipeline_spec.rb
index 3fb7e28036e..b9e424f4a7d 100644
--- a/spec/lib/bulk_imports/projects/pipelines/merge_requests_pipeline_spec.rb
+++ b/spec/lib/bulk_imports/projects/pipelines/merge_requests_pipeline_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe BulkImports::Projects::Pipelines::MergeRequestsPipeline do
+RSpec.describe BulkImports::Projects::Pipelines::MergeRequestsPipeline, feature_category: :importers do
let_it_be(:user) { create(:user) }
let_it_be(:another_user) { create(:user) }
let_it_be(:group) { create(:group) }
@@ -43,6 +43,7 @@ RSpec.describe BulkImports::Projects::Pipelines::MergeRequestsPipeline do
'base_commit_sha' => 'ae73cb07c9eeaf35924a10f713b364d32b2dd34f',
'head_commit_sha' => 'a97f74ddaa848b707bea65441c903ae4bf5d844d',
'start_commit_sha' => '9eea46b5c72ead701c22f516474b95049c9d9462',
+ 'diff_type' => 1,
'merge_request_diff_commits' => [
{
'sha' => 'COMMIT1',
@@ -99,6 +100,8 @@ RSpec.describe BulkImports::Projects::Pipelines::MergeRequestsPipeline do
allow(project.repository).to receive(:branch_exists?).and_return(false)
allow(project.repository).to receive(:create_branch)
+ allow(::Projects::ImportExport::AfterImportMergeRequestsWorker).to receive(:perform_async)
+
pipeline.run
end
@@ -244,8 +247,10 @@ RSpec.describe BulkImports::Projects::Pipelines::MergeRequestsPipeline do
expect(imported_mr.merge_request_diff).to be_present
end
- it 'has the correct data for merge request latest_merge_request_diff' do
- expect(imported_mr.latest_merge_request_diff_id).to eq(imported_mr.merge_request_diffs.maximum(:id))
+ it 'enqueues AfterImportMergeRequestsWorker worker' do
+ expect(::Projects::ImportExport::AfterImportMergeRequestsWorker)
+ .to have_received(:perform_async)
+ .with(project.id)
end
it 'imports diff files' do
diff --git a/spec/lib/bulk_imports/projects/pipelines/releases_pipeline_spec.rb b/spec/lib/bulk_imports/projects/pipelines/releases_pipeline_spec.rb
index 9e0b5af6bfe..fa85e24189c 100644
--- a/spec/lib/bulk_imports/projects/pipelines/releases_pipeline_spec.rb
+++ b/spec/lib/bulk_imports/projects/pipelines/releases_pipeline_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe BulkImports::Projects::Pipelines::ReleasesPipeline do
+RSpec.describe BulkImports::Projects::Pipelines::ReleasesPipeline, feature_category: :importers do
let_it_be(:user) { create(:user) }
let_it_be(:group) { create(:group) }
let_it_be(:project) { create(:project, group: group) }
diff --git a/spec/lib/bulk_imports/source_url_builder_spec.rb b/spec/lib/bulk_imports/source_url_builder_spec.rb
new file mode 100644
index 00000000000..2c0e042314b
--- /dev/null
+++ b/spec/lib/bulk_imports/source_url_builder_spec.rb
@@ -0,0 +1,78 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe BulkImports::SourceUrlBuilder, feature_category: :importers do
+ let_it_be(:bulk_import) { create(:bulk_import) }
+ let_it_be(:configuration) { create(:bulk_import_configuration, bulk_import: bulk_import) }
+
+ let(:entity) { create(:bulk_import_entity, bulk_import: bulk_import) }
+ let(:tracker) { create(:bulk_import_tracker, entity: entity) }
+ let(:context) { BulkImports::Pipeline::Context.new(tracker) }
+ let(:entry) { Issue.new(iid: 1, title: 'hello world') }
+
+ describe '#url' do
+ subject { described_class.new(context, entry) }
+
+ before do
+ allow(subject).to receive(:relation).and_return('issues')
+ end
+
+ context 'when relation is allowed' do
+ context 'when entity is a group' do
+ it 'returns the url specific to groups' do
+ expected_url = File.join(
+ configuration.url,
+ 'groups',
+ entity.source_full_path,
+ '-',
+ 'issues',
+ '1'
+ )
+
+ expect(subject.url).to eq(expected_url)
+ end
+ end
+
+ context 'when entity is a project' do
+ let(:entity) { create(:bulk_import_entity, :project_entity, bulk_import: bulk_import) }
+
+ it 'returns the url' do
+ expected_url = File.join(
+ configuration.url,
+ entity.source_full_path,
+ '-',
+ 'issues',
+ '1'
+ )
+
+ expect(subject.url).to eq(expected_url)
+ end
+ end
+ end
+
+ context 'when entry is not an ApplicationRecord' do
+ let(:entry) { 'not an ApplicationRecord' }
+
+ it 'returns nil' do
+ expect(subject.url).to be_nil
+ end
+ end
+
+ context 'when relation is not allowed' do
+ it 'returns nil' do
+ allow(subject).to receive(:relation).and_return('not_allowed')
+
+ expect(subject.url).to be_nil
+ end
+ end
+
+ context 'when entry has no iid' do
+ let(:entry) { Issue.new }
+
+ it 'returns nil' do
+ expect(subject.url).to be_nil
+ end
+ end
+ end
+end
diff --git a/spec/lib/click_house/models/audit_event_spec.rb b/spec/lib/click_house/models/audit_event_spec.rb
new file mode 100644
index 00000000000..ea3f1a6cbd4
--- /dev/null
+++ b/spec/lib/click_house/models/audit_event_spec.rb
@@ -0,0 +1,132 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe ClickHouse::Models::AuditEvent, feature_category: :audit_events do
+ let(:instance) { described_class.new }
+
+ describe '#by_entity_type' do
+ it 'builds the correct SQL' do
+ expected_sql = <<~SQL
+ SELECT * FROM "audit_events" WHERE "audit_events"."entity_type" = 'Project'
+ SQL
+
+ result_sql = instance.by_entity_type("Project").to_sql
+
+ expect(result_sql.strip).to eq(expected_sql.strip)
+ end
+ end
+
+ describe '#by_entity_id' do
+ it 'builds the correct SQL' do
+ expected_sql = <<~SQL
+ SELECT * FROM "audit_events" WHERE "audit_events"."entity_id" = 42
+ SQL
+
+ result_sql = instance.by_entity_id(42).to_sql
+
+ expect(result_sql.strip).to eq(expected_sql.strip)
+ end
+ end
+
+ describe '#by_author_id' do
+ it 'builds the correct SQL' do
+ expected_sql = <<~SQL
+ SELECT * FROM "audit_events" WHERE "audit_events"."author_id" = 5
+ SQL
+
+ result_sql = instance.by_author_id(5).to_sql
+
+ expect(result_sql.strip).to eq(expected_sql.strip)
+ end
+ end
+
+ describe '#by_entity_username' do
+ let_it_be(:user) { create(:user, username: 'Dummy') }
+
+ it 'builds the correct SQL' do
+ expected_sql = <<~SQL
+ SELECT * FROM "audit_events" WHERE "audit_events"."entity_id" = #{user.id}
+ SQL
+
+ result_sql = instance.by_entity_username('Dummy').to_sql
+
+ expect(result_sql.strip).to eq(expected_sql.strip)
+ end
+ end
+
+ describe '#by_author_username' do
+ let_it_be(:user) { create(:user, username: 'Dummy') }
+
+ it 'builds the correct SQL' do
+ expected_sql = <<~SQL
+ SELECT * FROM "audit_events" WHERE "audit_events"."author_id" = #{user.id}
+ SQL
+
+ result_sql = instance.by_author_username('Dummy').to_sql
+
+ expect(result_sql.strip).to eq(expected_sql.strip)
+ end
+ end
+
+ describe 'class methods' do
+ before do
+ allow(described_class).to receive(:new).and_return(instance)
+ end
+
+ describe '.by_entity_type' do
+ it 'calls the corresponding instance method' do
+ expect(instance).to receive(:by_entity_type).with("Project")
+
+ described_class.by_entity_type("Project")
+ end
+ end
+
+ describe '.by_entity_id' do
+ it 'calls the corresponding instance method' do
+ expect(instance).to receive(:by_entity_id).with(42)
+
+ described_class.by_entity_id(42)
+ end
+ end
+
+ describe '.by_author_id' do
+ it 'calls the corresponding instance method' do
+ expect(instance).to receive(:by_author_id).with(5)
+
+ described_class.by_author_id(5)
+ end
+ end
+
+ describe '.by_entity_username' do
+ it 'calls the corresponding instance method' do
+ expect(instance).to receive(:by_entity_username).with('Dummy')
+
+ described_class.by_entity_username('Dummy')
+ end
+ end
+
+ describe '.by_author_username' do
+ it 'calls the corresponding instance method' do
+ expect(instance).to receive(:by_author_username).with('Dummy')
+
+ described_class.by_author_username('Dummy')
+ end
+ end
+ end
+
+ describe 'method chaining' do
+ it 'builds the correct SQL with chained methods' do
+ expected_sql = <<~SQL.lines(chomp: true).join(' ')
+ SELECT * FROM "audit_events"
+ WHERE "audit_events"."entity_type" = 'Project'
+ AND "audit_events"."author_id" = 1
+ SQL
+
+ instance = described_class.new
+ result_sql = instance.by_entity_type("Project").by_author_id(1).to_sql
+
+ expect(result_sql.strip).to eq(expected_sql.strip)
+ end
+ end
+end
diff --git a/spec/lib/click_house/models/base_model_spec.rb b/spec/lib/click_house/models/base_model_spec.rb
new file mode 100644
index 00000000000..376300d7781
--- /dev/null
+++ b/spec/lib/click_house/models/base_model_spec.rb
@@ -0,0 +1,117 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe ClickHouse::Models::BaseModel, feature_category: :database do
+ let(:table_name) { "dummy_table" }
+ let(:query_builder) { instance_double("ClickHouse::QueryBuilder") }
+ let(:updated_query_builder) { instance_double("ClickHouse::QueryBuilder") }
+
+ let(:dummy_class) do
+ Class.new(described_class) do
+ def self.table_name
+ "dummy_table"
+ end
+ end
+ end
+
+ describe '#to_sql' do
+ it 'delegates to the query builder' do
+ expect(query_builder).to receive(:to_sql).and_return("SELECT * FROM dummy_table")
+
+ dummy_instance = dummy_class.new(query_builder)
+
+ expect(dummy_instance.to_sql).to eq("SELECT * FROM dummy_table")
+ end
+ end
+
+ describe '#where' do
+ it 'returns a new instance with refined query' do
+ dummy_instance = dummy_class.new(query_builder)
+
+ expect(query_builder).to receive(:where).with({ foo: "bar" }).and_return(updated_query_builder)
+
+ new_instance = dummy_instance.where(foo: "bar")
+
+ expect(new_instance).to be_a(dummy_class)
+ expect(new_instance).not_to eq(dummy_instance)
+ end
+ end
+
+ describe '#order' do
+ it 'returns a new instance with an order clause' do
+ dummy_instance = dummy_class.new(query_builder)
+
+ expect(query_builder).to receive(:order).with(:created_at, :asc).and_return(updated_query_builder)
+
+ new_instance = dummy_instance.order(:created_at)
+
+ expect(new_instance).to be_a(dummy_class)
+ expect(new_instance).not_to eq(dummy_instance)
+ end
+
+ context "when direction is also passed" do
+ it 'returns a new instance with an order clause' do
+ dummy_instance = dummy_class.new(query_builder)
+
+ expect(query_builder).to receive(:order).with(:created_at, :desc).and_return(updated_query_builder)
+
+ new_instance = dummy_instance.order(:created_at, :desc)
+
+ expect(new_instance).to be_a(dummy_class)
+ expect(new_instance).not_to eq(dummy_instance)
+ end
+ end
+ end
+
+ describe '#limit' do
+ it 'returns a new instance with a limit clause' do
+ dummy_instance = dummy_class.new(query_builder)
+
+ expect(query_builder).to receive(:limit).with(10).and_return(updated_query_builder)
+
+ new_instance = dummy_instance.limit(10)
+
+ expect(new_instance).to be_a(dummy_class)
+ expect(new_instance).not_to eq(dummy_instance)
+ end
+ end
+
+ describe '#offset' do
+ it 'returns a new instance with an offset clause' do
+ dummy_instance = dummy_class.new(query_builder)
+
+ expect(query_builder).to receive(:offset).with(5).and_return(updated_query_builder)
+
+ new_instance = dummy_instance.offset(5)
+
+ expect(new_instance).to be_a(dummy_class)
+ expect(new_instance).not_to eq(dummy_instance)
+ end
+ end
+
+ describe '#select' do
+ it 'returns a new instance with selected fields' do
+ dummy_instance = dummy_class.new(query_builder)
+
+ expect(query_builder).to receive(:select).with(:id, :name).and_return(updated_query_builder)
+
+ new_instance = dummy_instance.select(:id, :name)
+
+ expect(new_instance).to be_a(dummy_class)
+ expect(new_instance).not_to eq(dummy_instance)
+ end
+ end
+
+ describe '.table_name' do
+ it 'raises a NotImplementedError for the base model' do
+ expect do
+ described_class.table_name
+ end.to raise_error(NotImplementedError, "Subclasses must define a `table_name` class method")
+ end
+
+ it 'does not raise an error for the subclass' do
+ expect(dummy_class.table_name).to eq(table_name)
+ end
+ end
+end
diff --git a/spec/lib/container_registry/client_spec.rb b/spec/lib/container_registry/client_spec.rb
index 39409cf8d3a..37161119744 100644
--- a/spec/lib/container_registry/client_spec.rb
+++ b/spec/lib/container_registry/client_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe ContainerRegistry::Client do
+RSpec.describe ContainerRegistry::Client, feature_category: :container_registry do
using RSpec::Parameterized::TableSyntax
include_context 'container registry client'
@@ -307,12 +307,12 @@ RSpec.describe ContainerRegistry::Client do
end
end
- describe '#delete_repository_tag_by_name' do
- subject { client.delete_repository_tag_by_name('group/test', 'a') }
+ describe '#delete_repository_tag_by_digest' do
+ subject { client.delete_repository_tag_by_digest('group/test', 'a') }
context 'when the tag exists' do
before do
- stub_request(:delete, "http://container-registry/v2/group/test/tags/reference/a")
+ stub_request(:delete, "http://container-registry/v2/group/test/manifests/a")
.with(headers: headers_with_accept_types)
.to_return(status: 200, body: "")
end
@@ -322,7 +322,7 @@ RSpec.describe ContainerRegistry::Client do
context 'when the tag does not exist' do
before do
- stub_request(:delete, "http://container-registry/v2/group/test/tags/reference/a")
+ stub_request(:delete, "http://container-registry/v2/group/test/manifests/a")
.with(headers: headers_with_accept_types)
.to_return(status: 404, body: "")
end
@@ -332,7 +332,7 @@ RSpec.describe ContainerRegistry::Client do
context 'when an error occurs' do
before do
- stub_request(:delete, "http://container-registry/v2/group/test/tags/reference/a")
+ stub_request(:delete, "http://container-registry/v2/group/test/manifests/a")
.with(headers: headers_with_accept_types)
.to_return(status: 500, body: "")
end
@@ -485,7 +485,7 @@ RSpec.describe ContainerRegistry::Client do
def stub_registry_tags_support(supported = true)
status_code = supported ? 200 : 404
- stub_request(:options, "#{registry_api_url}/v2/name/tags/reference/tag")
+ stub_request(:options, "#{registry_api_url}/v2/name/manifests/tag")
.to_return(
status: status_code,
body: '',
diff --git a/spec/lib/container_registry/gitlab_api_client_spec.rb b/spec/lib/container_registry/gitlab_api_client_spec.rb
index 86675ba27f6..3c87af3a1c8 100644
--- a/spec/lib/container_registry/gitlab_api_client_spec.rb
+++ b/spec/lib/container_registry/gitlab_api_client_spec.rb
@@ -220,6 +220,7 @@ RSpec.describe ContainerRegistry::GitlabApiClient, feature_category: :container_
{
name: '0.1.0',
digest: 'sha256:1234567890',
+ config_digest: 'sha256:13828381121',
media_type: 'application/vnd.oci.image.manifest.v1+json',
size_bytes: 1234567890,
created_at: 5.minutes.ago
@@ -227,6 +228,7 @@ RSpec.describe ContainerRegistry::GitlabApiClient, feature_category: :container_
{
name: 'latest',
digest: 'sha256:1234567892',
+ config_digest: 'sha256:33139438113',
media_type: 'application/vnd.oci.image.manifest.v1+json',
size_bytes: 1234567892,
created_at: 10.minutes.ago
diff --git a/spec/lib/container_registry/tag_spec.rb b/spec/lib/container_registry/tag_spec.rb
index cb5c6a60e1d..8f9308f2127 100644
--- a/spec/lib/container_registry/tag_spec.rb
+++ b/spec/lib/container_registry/tag_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe ContainerRegistry::Tag do
+RSpec.describe ContainerRegistry::Tag, feature_category: :container_registry do
let(:group) { create(:group, name: 'group') }
let(:project) { create(:project, path: 'test', group: group) }
@@ -74,6 +74,77 @@ RSpec.describe ContainerRegistry::Tag do
end
end
+ describe '#total_size' do
+ context 'when total_size is set' do
+ before do
+ tag.total_size = 1000
+ end
+
+ it 'returns the set size' do
+ expect(tag.total_size).to eq(1000)
+ end
+ end
+ end
+
+ describe '#revision' do
+ context 'when revision is set' do
+ before do
+ tag.revision = 'xyz789'
+ end
+
+ it 'returns the set revision' do
+ expect(tag.revision).to eq('xyz789')
+ end
+ end
+
+ context 'when revision is not set' do
+ context 'when config_blob is not nil' do
+ let(:blob) { ContainerRegistry::Blob.new(repository, {}) }
+
+ before do
+ allow(tag).to receive(:config_blob).and_return(blob)
+ allow(blob).to receive(:revision).and_return('abc123')
+ end
+
+ it 'returns the revision from config_blob' do
+ expect(tag.revision).to eq('abc123')
+ end
+ end
+
+ context 'when config_blob is nil' do
+ before do
+ allow(tag).to receive(:config_blob).and_return(nil)
+ end
+
+ it 'returns nil' do
+ expect(tag.revision).to be_nil
+ end
+ end
+ end
+ end
+
+ describe '#short_revision' do
+ context 'when revision is not nil' do
+ before do
+ allow(tag).to receive(:revision).and_return('abcdef1234567890')
+ end
+
+ it 'returns the first 9 characters of the revision' do
+ expect(tag.short_revision).to eq('abcdef123')
+ end
+ end
+
+ context 'when revision is nil' do
+ before do
+ allow(tag).to receive(:revision).and_return(nil)
+ end
+
+ it 'returns nil' do
+ expect(tag.short_revision).to be_nil
+ end
+ end
+ end
+
context 'schema v1' do
before do
stub_request(:get, 'http://registry.gitlab/v2/group/test/manifests/tag')
@@ -277,6 +348,16 @@ RSpec.describe ContainerRegistry::Tag do
end
describe '#digest' do
+ context 'when manifest_digest is set' do
+ before do
+ tag.manifest_digest = 'sha256:manifestdigest'
+ end
+
+ it 'returns the set manifest_digest' do
+ expect(tag.digest).to eq('sha256:manifestdigest')
+ end
+ end
+
it 'returns a correct tag digest' do
expect(tag.digest).to eq 'sha256:digest'
end
diff --git a/spec/lib/generators/batched_background_migration/batched_background_migration_generator_spec.rb b/spec/lib/generators/batched_background_migration/batched_background_migration_generator_spec.rb
index 2d48b83be4c..893cf976074 100644
--- a/spec/lib/generators/batched_background_migration/batched_background_migration_generator_spec.rb
+++ b/spec/lib/generators/batched_background_migration/batched_background_migration_generator_spec.rb
@@ -14,6 +14,7 @@ RSpec.describe BatchedBackgroundMigration::BatchedBackgroundMigrationGenerator,
before do
prepare_destination
+ allow(Gitlab).to receive(:current_milestone).and_return('16.6')
end
after do
diff --git a/spec/lib/generators/batched_background_migration/expected_files/queue_my_batched_migration.txt b/spec/lib/generators/batched_background_migration/expected_files/queue_my_batched_migration.txt
index aa79062422b..36f7885b591 100644
--- a/spec/lib/generators/batched_background_migration/expected_files/queue_my_batched_migration.txt
+++ b/spec/lib/generators/batched_background_migration/expected_files/queue_my_batched_migration.txt
@@ -5,7 +5,9 @@
# Update below commented lines with appropriate values.
-class QueueMyBatchedMigration < Gitlab::Database::Migration[2.1]
+class QueueMyBatchedMigration < Gitlab::Database::Migration[2.2]
+ milestone '16.6'
+
MIGRATION = "MyBatchedMigration"
# DELAY_INTERVAL = 2.minutes
# BATCH_SIZE = 1000
diff --git a/spec/lib/generators/gitlab/snowplow_event_definition_generator_spec.rb b/spec/lib/generators/gitlab/snowplow_event_definition_generator_spec.rb
deleted file mode 100644
index 740cfa767e4..00000000000
--- a/spec/lib/generators/gitlab/snowplow_event_definition_generator_spec.rb
+++ /dev/null
@@ -1,95 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Gitlab::SnowplowEventDefinitionGenerator, :silence_stdout, feature_category: :product_analytics_data_management do
- let(:ce_temp_dir) { Dir.mktmpdir }
- let(:ee_temp_dir) { Dir.mktmpdir }
- let(:timestamp) { Time.now.utc.strftime('%Y%m%d%H%M%S') }
-
- let(:generator_options) do
- { 'category' => 'Projects::Pipelines::EmailCampaignsController', 'action' => 'click' }
- end
-
- before do
- stub_const("#{described_class}::CE_DIR", ce_temp_dir)
- stub_const("#{described_class}::EE_DIR", ee_temp_dir)
- end
-
- around do |example|
- freeze_time { example.run }
- end
-
- after do
- FileUtils.rm_rf([ce_temp_dir, ee_temp_dir])
- end
-
- describe 'Creating event definition file' do
- before do
- stub_const('Gitlab::VERSION', '13.11.0-pre')
- end
-
- let(:sample_event_dir) { 'lib/generators/gitlab/snowplow_event_definition_generator' }
- let(:file_name) { Dir.children(ce_temp_dir).first }
-
- it 'creates CE event definition file using the template' do
- sample_event = ::Gitlab::Config::Loader::Yaml
- .new(fixture_file(File.join(sample_event_dir, 'sample_event.yml'))).load_raw!
-
- described_class.new([], generator_options).invoke_all
-
- event_definition_path = File.join(ce_temp_dir, file_name)
- expect(::Gitlab::Config::Loader::Yaml.new(File.read(event_definition_path)).load_raw!).to eq(sample_event)
- end
-
- describe 'generated filename' do
- it 'includes timestamp' do
- described_class.new([], generator_options).invoke_all
-
- expect(file_name).to include(timestamp.to_s)
- end
-
- it 'removes special characters' do
- generator_options = { 'category' => '"`ui:[mavenpackages | t5%348()-=@ ]`"', 'action' => 'click' }
-
- described_class.new([], generator_options).invoke_all
-
- expect(file_name).to include('uimavenpackagest')
- end
-
- it 'cuts name if longer than 100 characters' do
- generator_options = { 'category' => 'a' * 100, 'action' => 'click' }
-
- described_class.new([], generator_options).invoke_all
-
- expect(file_name.length).to eq(100)
- end
- end
-
- context 'when event definition with same file name already exists' do
- before do
- stub_const('Gitlab::VERSION', '12.11.0-pre')
- described_class.new([], generator_options).invoke_all
- end
-
- it 'raises error' do
- expect { described_class.new([], generator_options.merge('force' => false)).invoke_all }
- .to raise_error(StandardError, /Event definition already exists at/)
- end
- end
-
- describe 'EE' do
- let(:file_name) { Dir.children(ee_temp_dir).first }
-
- it 'creates EE event definition file using the template' do
- sample_event = ::Gitlab::Config::Loader::Yaml
- .new(fixture_file(File.join(sample_event_dir, 'sample_event_ee.yml'))).load_raw!
-
- described_class.new([], generator_options.merge('ee' => true)).invoke_all
-
- event_definition_path = File.join(ee_temp_dir, file_name)
- expect(::Gitlab::Config::Loader::Yaml.new(File.read(event_definition_path)).load_raw!).to eq(sample_event)
- end
- end
- end
-end
diff --git a/spec/lib/generators/gitlab/usage_metric_definition/redis_hll_generator_spec.rb b/spec/lib/generators/gitlab/usage_metric_definition/redis_hll_generator_spec.rb
index b6e1d59f6c0..5265b608ab4 100644
--- a/spec/lib/generators/gitlab/usage_metric_definition/redis_hll_generator_spec.rb
+++ b/spec/lib/generators/gitlab/usage_metric_definition/redis_hll_generator_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::UsageMetricDefinition::RedisHllGenerator, :silence_stdout do
+RSpec.describe Gitlab::UsageMetricDefinition::RedisHllGenerator, :silence_stdout, feature_category: :service_ping do
include UsageDataHelpers
let(:category) { 'test_category' }
@@ -16,6 +16,10 @@ RSpec.describe Gitlab::UsageMetricDefinition::RedisHllGenerator, :silence_stdout
stub_const("#{Gitlab::UsageMetricDefinitionGenerator}::TOP_LEVEL_DIR", temp_dir)
# Stub Prometheus requests from Gitlab::Utils::UsageData
stub_prometheus_queries
+
+ allow_next_instance_of(Gitlab::UsageMetricDefinitionGenerator) do |instance|
+ allow(instance).to receive(:ask).and_return('y') # confirm deprecation warning
+ end
end
after do
diff --git a/spec/lib/generators/gitlab/usage_metric_definition_generator_spec.rb b/spec/lib/generators/gitlab/usage_metric_definition_generator_spec.rb
index f7a4bac39d7..e0cb74d8559 100644
--- a/spec/lib/generators/gitlab/usage_metric_definition_generator_spec.rb
+++ b/spec/lib/generators/gitlab/usage_metric_definition_generator_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::UsageMetricDefinitionGenerator, :silence_stdout do
+RSpec.describe Gitlab::UsageMetricDefinitionGenerator, :silence_stdout, feature_category: :service_ping do
include UsageDataHelpers
let(:key_path) { 'counts_weekly.test_metric' }
@@ -14,6 +14,10 @@ RSpec.describe Gitlab::UsageMetricDefinitionGenerator, :silence_stdout do
stub_const("#{described_class}::TOP_LEVEL_DIR", temp_dir)
# Stub Prometheus requests from Gitlab::Utils::UsageData
stub_prometheus_queries
+
+ allow_next_instance_of(described_class) do |instance|
+ allow(instance).to receive(:ask).and_return('y') # confirm deprecation warning
+ end
end
after do
@@ -100,4 +104,19 @@ RSpec.describe Gitlab::UsageMetricDefinitionGenerator, :silence_stdout do
expect(files.count).to eq(2)
end
end
+
+ ['n', 'N', 'random word', nil].each do |answer|
+ context "when user agreed with deprecation warning by typing: #{answer}" do
+ it 'does not create definition file' do
+ allow_next_instance_of(described_class) do |instance|
+ allow(instance).to receive(:ask).and_return(answer)
+ end
+
+ described_class.new([key_path], { 'dir' => dir, 'class_name' => class_name }).invoke_all
+ files = Dir.glob(File.join(temp_dir, 'metrics/counts_7d/*_metric.yml'))
+
+ expect(files.count).to eq(0)
+ end
+ end
+ end
end
diff --git a/spec/lib/generators/model/mocks/migration_file.txt b/spec/lib/generators/model/mocks/migration_file.txt
index c9e51e51863..d0a5c71ffc3 100644
--- a/spec/lib/generators/model/mocks/migration_file.txt
+++ b/spec/lib/generators/model/mocks/migration_file.txt
@@ -3,7 +3,7 @@
# See https://docs.gitlab.com/ee/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
-class CreateModelGeneratorTestFoos < Gitlab::Database::Migration[2.1]
+class CreateModelGeneratorTestFoos < Gitlab::Database::Migration[2.2]
# When using the methods "add_concurrent_index" or "remove_concurrent_index"
# you must disable the use of transactions
# as these methods can not run in an existing transaction.
@@ -16,6 +16,7 @@ class CreateModelGeneratorTestFoos < Gitlab::Database::Migration[2.1]
# To disable transactions uncomment the following line and remove these
# comments:
# disable_ddl_transaction!
+ milestone '16.5'
# Add dependent 'batched_background_migrations.queued_migration_version' values.
# DEPENDENT_BATCHED_BACKGROUND_MIGRATIONS = []
diff --git a/spec/lib/generators/model/model_generator_spec.rb b/spec/lib/generators/model/model_generator_spec.rb
index 0e770190d25..7284fc8b28a 100644
--- a/spec/lib/generators/model/model_generator_spec.rb
+++ b/spec/lib/generators/model/model_generator_spec.rb
@@ -17,6 +17,10 @@ RSpec.describe Model::ModelGenerator do
FileUtils.rm_rf(temp_dir)
end
+ before do
+ allow(Gitlab).to receive(:current_milestone).and_return('16.5')
+ end
+
it 'creates the model file with the right content' do
subject.invoke_all
diff --git a/spec/lib/gitlab/alert_management/payload/base_spec.rb b/spec/lib/gitlab/alert_management/payload/base_spec.rb
index 3e8d71ac673..bfde0a69f98 100644
--- a/spec/lib/gitlab/alert_management/payload/base_spec.rb
+++ b/spec/lib/gitlab/alert_management/payload/base_spec.rb
@@ -32,7 +32,7 @@ RSpec.describe Gitlab::AlertManagement::Payload::Base do
context 'with multiple paths provided' do
let(:payload_class) do
Class.new(described_class) do
- attribute :test, paths: [['test'], %w(alt test)]
+ attribute :test, paths: [['test'], %w[alt test]]
end
end
@@ -204,8 +204,8 @@ RSpec.describe Gitlab::AlertManagement::Payload::Base do
end
context 'with too-long hosts array' do
- let(:hosts) { %w(abc def ghij) }
- let(:shortened_hosts) { %w(abc def ghi) }
+ let(:hosts) { %w[abc def ghij] }
+ let(:shortened_hosts) { %w[abc def ghi] }
before do
stub_const('::AlertManagement::Alert::HOSTS_MAX_LENGTH', 9)
@@ -215,15 +215,15 @@ RSpec.describe Gitlab::AlertManagement::Payload::Base do
it { is_expected.to eq(hosts: shortened_hosts, project_id: project.id) }
context 'with host cut off between elements' do
- let(:hosts) { %w(abcde fghij) }
- let(:shortened_hosts) { %w(abcde fghi) }
+ let(:hosts) { %w[abcde fghij] }
+ let(:shortened_hosts) { %w[abcde fghi] }
it { is_expected.to eq({ hosts: shortened_hosts, project_id: project.id }) }
end
context 'with nested hosts' do
let(:hosts) { ['abc', ['de', 'f'], 'g', 'hij'] } # rubocop:disable Style/WordArray
- let(:shortened_hosts) { %w(abc de f g hi) }
+ let(:shortened_hosts) { %w[abc de f g hi] }
it { is_expected.to eq({ hosts: shortened_hosts, project_id: project.id }) }
end
diff --git a/spec/lib/gitlab/analytics/cycle_analytics/average_spec.rb b/spec/lib/gitlab/analytics/cycle_analytics/average_spec.rb
index 261d587506f..b2a267d42ec 100644
--- a/spec/lib/gitlab/analytics/cycle_analytics/average_spec.rb
+++ b/spec/lib/gitlab/analytics/cycle_analytics/average_spec.rb
@@ -4,7 +4,6 @@ require 'spec_helper'
RSpec.describe Gitlab::Analytics::CycleAnalytics::Average, feature_category: :value_stream_management do
let_it_be(:project) { create(:project) }
-
let_it_be(:issue_1) do
# Duration: 10 days
create(:issue, project: project, created_at: 20.days.ago).tap do |issue|
@@ -30,8 +29,12 @@ RSpec.describe Gitlab::Analytics::CycleAnalytics::Average, feature_category: :va
let(:query) { Issue.joins(:metrics).in_projects(project.id) }
- around do |example|
- freeze_time { example.run }
+ before_all do
+ freeze_time
+ end
+
+ after :all do
+ unfreeze_time
end
subject(:average) { described_class.new(stage: stage, query: query) }
@@ -45,8 +48,7 @@ RSpec.describe Gitlab::Analytics::CycleAnalytics::Average, feature_category: :va
it { is_expected.to eq(nil) }
end
- context 'returns the average duration in seconds',
- quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/413223' do
+ context 'returns the average duration in seconds' do
it { is_expected.to be_within(0.5).of(7.5.days.to_f) }
end
end
diff --git a/spec/lib/gitlab/asset_proxy_spec.rb b/spec/lib/gitlab/asset_proxy_spec.rb
index 7d7952d5741..af8721739a0 100644
--- a/spec/lib/gitlab/asset_proxy_spec.rb
+++ b/spec/lib/gitlab/asset_proxy_spec.rb
@@ -17,7 +17,7 @@ RSpec.describe Gitlab::AssetProxy do
context 'when asset proxy is enabled' do
before do
- stub_asset_proxy_setting(allowlist: %w(gitlab.com *.mydomain.com))
+ stub_asset_proxy_setting(allowlist: %w[gitlab.com *.mydomain.com])
stub_asset_proxy_setting(
enabled: true,
url: 'https://assets.example.com',
diff --git a/spec/lib/gitlab/auth/ldap/auth_hash_spec.rb b/spec/lib/gitlab/auth/ldap/auth_hash_spec.rb
index c19d890a703..0208255d24d 100644
--- a/spec/lib/gitlab/auth/ldap/auth_hash_spec.rb
+++ b/spec/lib/gitlab/auth/ldap/auth_hash_spec.rb
@@ -52,7 +52,7 @@ RSpec.describe Gitlab::Auth::Ldap::AuthHash do
let(:attributes) do
{
- 'username' => %w(mail email),
+ 'username' => %w[mail email],
'name' => 'fullName'
}
end
diff --git a/spec/lib/gitlab/auth/ldap/config_spec.rb b/spec/lib/gitlab/auth/ldap/config_spec.rb
index 48039b58216..f97b16254e7 100644
--- a/spec/lib/gitlab/auth/ldap/config_spec.rb
+++ b/spec/lib/gitlab/auth/ldap/config_spec.rb
@@ -90,7 +90,7 @@ AtlErSqafbECNDSwS5BX8yDpu5yRBJ4xegO/rNlmb8ICRYkuJapD1xXicFOsmfUK
end
it 'returns one provider' do
- expect(described_class.available_providers).to match_array(%w(ldapmain))
+ expect(described_class.available_providers).to match_array(%w[ldapmain])
end
end
@@ -552,15 +552,15 @@ AtlErSqafbECNDSwS5BX8yDpu5yRBJ4xegO/rNlmb8ICRYkuJapD1xXicFOsmfUK
stub_ldap_config(
options: {
'attributes' => {
- 'username' => %w(sAMAccountName),
- 'email' => %w(userPrincipalName)
+ 'username' => %w[sAMAccountName],
+ 'email' => %w[userPrincipalName]
}
}
)
expect(config.attributes).to include({
- 'username' => %w(sAMAccountName),
- 'email' => %w(userPrincipalName),
+ 'username' => %w[sAMAccountName],
+ 'email' => %w[userPrincipalName],
'name' => 'cn'
})
end
diff --git a/spec/lib/gitlab/auth/ldap/person_spec.rb b/spec/lib/gitlab/auth/ldap/person_spec.rb
index f8268bb1666..b5fd44d4aa9 100644
--- a/spec/lib/gitlab/auth/ldap/person_spec.rb
+++ b/spec/lib/gitlab/auth/ldap/person_spec.rb
@@ -13,13 +13,13 @@ RSpec.describe Gitlab::Auth::Ldap::Person do
'uid' => 'uid',
'attributes' => {
'name' => 'cn',
- 'email' => %w(mail email userPrincipalName),
+ 'email' => %w[mail email userPrincipalName],
'username' => username_attribute
}
}
)
end
- let(:username_attribute) { %w(uid sAMAccountName userid) }
+ let(:username_attribute) { %w[uid sAMAccountName userid] }
describe '.normalize_dn' do
subject { described_class.normalize_dn(given) }
@@ -57,7 +57,7 @@ RSpec.describe Gitlab::Auth::Ldap::Person do
'attributes' => {
'name' => 'cn',
'email' => 'mail',
- 'username' => %w(uid mail),
+ 'username' => %w[uid mail],
'first_name' => ''
}
}
diff --git a/spec/lib/gitlab/auth/o_auth/user_spec.rb b/spec/lib/gitlab/auth/o_auth/user_spec.rb
index 8a9182f6457..c137ca88589 100644
--- a/spec/lib/gitlab/auth/o_auth/user_spec.rb
+++ b/spec/lib/gitlab/auth/o_auth/user_spec.rb
@@ -369,7 +369,7 @@ RSpec.describe Gitlab::Auth::OAuth::User, feature_category: :system_access do
context "and at least one LDAP provider is defined" do
before do
- stub_ldap_config(providers: %w(ldapmain))
+ stub_ldap_config(providers: %w[ldapmain])
end
context "and a corresponding LDAP person" do
@@ -570,7 +570,7 @@ RSpec.describe Gitlab::Auth::OAuth::User, feature_category: :system_access do
before do
allow(ldap_user).to receive(:uid) { uid }
allow(ldap_user).to receive(:username) { 'johndoe@example.com' }
- allow(ldap_user).to receive(:email) { %w(johndoe@example.com john2@example.com) }
+ allow(ldap_user).to receive(:email) { %w[johndoe@example.com john2@example.com] }
allow(ldap_user).to receive(:dn) { dn }
end
@@ -605,7 +605,7 @@ RSpec.describe Gitlab::Auth::OAuth::User, feature_category: :system_access do
context "and at least one LDAP provider is defined" do
before do
- stub_ldap_config(providers: %w(ldapmain))
+ stub_ldap_config(providers: %w[ldapmain])
end
context "and a corresponding LDAP person" do
@@ -1055,7 +1055,7 @@ RSpec.describe Gitlab::Auth::OAuth::User, feature_category: :system_access do
context "update only requested info" do
before do
stub_omniauth_setting(sync_profile_from_provider: ['my-provider'])
- stub_omniauth_setting(sync_profile_attributes: %w(name location))
+ stub_omniauth_setting(sync_profile_attributes: %w[name location])
end
it "updates the user name" do
diff --git a/spec/lib/gitlab/auth/saml/auth_hash_spec.rb b/spec/lib/gitlab/auth/saml/auth_hash_spec.rb
index 5286e22abc9..e37b9b10834 100644
--- a/spec/lib/gitlab/auth/saml/auth_hash_spec.rb
+++ b/spec/lib/gitlab/auth/saml/auth_hash_spec.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe Gitlab::Auth::Saml::AuthHash do
include LoginHelpers
- let(:raw_info_attr) { { 'groups' => %w(Developers Freelancers) } }
+ let(:raw_info_attr) { { 'groups' => %w[Developers Freelancers] } }
subject(:saml_auth_hash) { described_class.new(omniauth_auth_hash) }
let(:info_hash) do
@@ -23,12 +23,12 @@ RSpec.describe Gitlab::Auth::Saml::AuthHash do
end
before do
- stub_saml_group_config(%w(Developers Freelancers Designers))
+ stub_saml_group_config(%w[Developers Freelancers Designers])
end
describe '#groups' do
it 'returns array of groups' do
- expect(saml_auth_hash.groups).to eq(%w(Developers Freelancers))
+ expect(saml_auth_hash.groups).to eq(%w[Developers Freelancers])
end
context 'raw info hash attributes empty' do
diff --git a/spec/lib/gitlab/auth/saml/config_spec.rb b/spec/lib/gitlab/auth/saml/config_spec.rb
index d657622c9f2..c19171bb6f8 100644
--- a/spec/lib/gitlab/auth/saml/config_spec.rb
+++ b/spec/lib/gitlab/auth/saml/config_spec.rb
@@ -3,6 +3,8 @@
require 'spec_helper'
RSpec.describe Gitlab::Auth::Saml::Config do
+ include LoginHelpers
+
describe '.enabled?' do
subject { described_class.enabled? }
@@ -10,13 +12,48 @@ RSpec.describe Gitlab::Auth::Saml::Config do
context 'when SAML is enabled' do
before do
- allow(Gitlab::Auth::OAuth::Provider).to receive(:providers).and_return([:saml])
+ stub_basic_saml_config
end
it { is_expected.to eq(true) }
end
end
+ describe '.default_attribute_statements' do
+ it 'includes upstream defaults, nickname and Microsoft values' do
+ expect(described_class.default_attribute_statements).to match_array(
+ {
+ nickname: %w[username nickname],
+ name: [
+ 'name',
+ 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name',
+ 'http://schemas.microsoft.com/ws/2008/06/identity/claims/name'
+ ],
+ email: [
+ 'email',
+ 'mail',
+ 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress',
+ 'http://schemas.microsoft.com/ws/2008/06/identity/claims/emailaddress'
+ ],
+ first_name: [
+ 'first_name',
+ 'firstname',
+ 'firstName',
+ 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname',
+ 'http://schemas.microsoft.com/ws/2008/06/identity/claims/givenname'
+ ],
+ last_name: [
+ 'last_name',
+ 'lastname',
+ 'lastName',
+ 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname',
+ 'http://schemas.microsoft.com/ws/2008/06/identity/claims/surname'
+ ]
+ }
+ )
+ end
+ end
+
describe '#external_groups' do
let(:config_1) { described_class.new('saml1') }
diff --git a/spec/lib/gitlab/auth/saml/user_spec.rb b/spec/lib/gitlab/auth/saml/user_spec.rb
index a8a5d8ae5df..034d1a69a0b 100644
--- a/spec/lib/gitlab/auth/saml/user_spec.rb
+++ b/spec/lib/gitlab/auth/saml/user_spec.rb
@@ -11,7 +11,7 @@ RSpec.describe Gitlab::Auth::Saml::User do
let(:uid) { 'my-uid' }
let(:dn) { 'uid=user1,ou=people,dc=example' }
let(:provider) { 'saml' }
- let(:raw_info_attr) { { 'groups' => %w(Developers Freelancers Designers) } }
+ let(:raw_info_attr) { { 'groups' => %w[Developers Freelancers Designers] } }
let(:auth_hash) { OmniAuth::AuthHash.new(uid: uid, provider: provider, info: info_hash, extra: { raw_info: OneLogin::RubySaml::Attributes.new(raw_info_attr) }) }
let(:info_hash) do
{
@@ -47,12 +47,12 @@ RSpec.describe Gitlab::Auth::Saml::User do
context 'external groups' do
before do
- stub_saml_group_config(%w(Interns))
+ stub_saml_group_config(%w[Interns])
end
context 'are defined' do
it 'marks the user as external' do
- stub_saml_group_config(%w(Freelancers))
+ stub_saml_group_config(%w[Freelancers])
saml_user.save # rubocop:disable Rails/SaveBang
expect(gl_user).to be_valid
expect(gl_user.external).to be_truthy
@@ -119,7 +119,7 @@ RSpec.describe Gitlab::Auth::Saml::User do
context 'external groups' do
context 'are defined' do
it 'marks the user as external' do
- stub_saml_group_config(%w(Freelancers))
+ stub_saml_group_config(%w[Freelancers])
saml_user.save # rubocop:disable Rails/SaveBang
expect(gl_user).to be_valid
expect(gl_user.external).to be_truthy
@@ -128,7 +128,7 @@ RSpec.describe Gitlab::Auth::Saml::User do
context 'are defined but the user does not belong there' do
it 'does not mark the user as external' do
- stub_saml_group_config(%w(Interns))
+ stub_saml_group_config(%w[Interns])
saml_user.save # rubocop:disable Rails/SaveBang
expect(gl_user).to be_valid
expect(gl_user.external).to be_falsey
@@ -151,7 +151,7 @@ RSpec.describe Gitlab::Auth::Saml::User do
context 'and at least one LDAP provider is defined' do
before do
- stub_ldap_config(providers: %w(ldapmain))
+ stub_ldap_config(providers: %w[ldapmain])
end
context 'and a corresponding LDAP person' do
@@ -160,7 +160,7 @@ RSpec.describe Gitlab::Auth::Saml::User do
before do
allow(ldap_user).to receive(:uid) { uid }
allow(ldap_user).to receive(:username) { uid }
- allow(ldap_user).to receive(:email) { %w(john@mail.com john2@example.com) }
+ allow(ldap_user).to receive(:email) { %w[john@mail.com john2@example.com] }
allow(ldap_user).to receive(:dn) { dn }
allow(Gitlab::Auth::Ldap::Adapter).to receive(:new).and_return(adapter)
allow(Gitlab::Auth::Ldap::Person).to receive(:find_by_uid).with(uid, adapter).and_return(ldap_user)
@@ -190,14 +190,14 @@ RSpec.describe Gitlab::Auth::Saml::User do
info: info_hash,
extra: {
raw_info: OneLogin::RubySaml::Attributes.new(
- { 'groups' => %w(Developers Freelancers Designers) }
+ { 'groups' => %w[Developers Freelancers Designers] }
)
}
}
end
let(:auth_hash) { OmniAuth::AuthHash.new(auth_hash_base_attributes) }
- let(:uid_types) { %w(uid dn email) }
+ let(:uid_types) { %w[uid dn email] }
before do
create(:omniauth_user,
@@ -410,7 +410,7 @@ RSpec.describe Gitlab::Auth::Saml::User do
let(:raw_info_attr) { {} }
it 'does not mark user as external' do
- stub_saml_group_config(%w(Freelancers))
+ stub_saml_group_config(%w[Freelancers])
expect(saml_user.find_user.external).to be_falsy
end
diff --git a/spec/lib/gitlab/auth_spec.rb b/spec/lib/gitlab/auth_spec.rb
index f5b9555916c..020089b3880 100644
--- a/spec/lib/gitlab/auth_spec.rb
+++ b/spec/lib/gitlab/auth_spec.rb
@@ -8,6 +8,8 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching, feature_cate
let(:auth_failure) { { actor: nil, project: nil, type: nil, authentication_abilities: nil } }
let(:gl_auth) { described_class }
+ let(:request) { instance_double(ActionDispatch::Request, ip: 'ip') }
+
describe 'constants' do
it 'API_SCOPES contains all scopes for API access' do
expect(subject::API_SCOPES).to match_array %i[api read_user read_api create_runner k8s_proxy]
@@ -202,7 +204,7 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching, feature_cate
end
context 'when IP is already banned' do
- subject { gl_auth.find_for_git_client('username-does-not-matter', 'password-does-not-matter', project: nil, ip: 'ip') }
+ subject { gl_auth.find_for_git_client('username-does-not-matter', 'password-does-not-matter', project: nil, request: request) }
before do
expect_next_instance_of(Gitlab::Auth::IpRateLimiter) do |rate_limiter|
@@ -223,7 +225,7 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching, feature_cate
expect(rate_limiter).not_to receive(:reset!)
end
- gl_auth.find_for_git_client('gitlab-ci-token', build.token, project: build.project, ip: 'ip')
+ gl_auth.find_for_git_client('gitlab-ci-token', build.token, project: build.project, request: request)
end
it 'skips rate limiting for failed auth' do
@@ -231,7 +233,7 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching, feature_cate
expect(rate_limiter).not_to receive(:register_fail!)
end
- gl_auth.find_for_git_client('gitlab-ci-token', 'wrong_token', project: build.project, ip: 'ip')
+ gl_auth.find_for_git_client('gitlab-ci-token', 'wrong_token', project: build.project, request: request)
end
end
@@ -243,7 +245,7 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching, feature_cate
expect(rate_limiter).to receive(:reset!)
end
- gl_auth.find_for_git_client(user.username, user.password, project: nil, ip: 'ip')
+ gl_auth.find_for_git_client(user.username, user.password, project: nil, request: request)
end
it 'rate limits a user by unique IPs' do
@@ -252,7 +254,7 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching, feature_cate
end
expect(Gitlab::Auth::UniqueIpsLimiter).to receive(:limit_user!).twice.and_call_original
- gl_auth.find_for_git_client(user.username, user.password, project: nil, ip: 'ip')
+ gl_auth.find_for_git_client(user.username, user.password, project: nil, request: request)
end
it 'registers failure for failed auth' do
@@ -260,13 +262,36 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching, feature_cate
expect(rate_limiter).to receive(:register_fail!)
end
- gl_auth.find_for_git_client(user.username, 'wrong_password', project: nil, ip: 'ip')
+ gl_auth.find_for_git_client(user.username, 'wrong_password', project: nil, request: request)
+ end
+
+ context 'when failure goes over threshold' do
+ let(:request) { instance_double(ActionDispatch::Request, fullpath: '/some/project.git/info/refs', request_method: 'GET', ip: 'ip') }
+
+ before do
+ expect_next_instance_of(Gitlab::Auth::IpRateLimiter) do |rate_limiter|
+ expect(rate_limiter).to receive(:register_fail!).and_return(true)
+ end
+ end
+
+ it 'logs a message' do
+ expect(Gitlab::AuthLogger).to receive(:error).with(
+ message: include('IP has been temporarily banned from Git auth'),
+ env: :blocklist,
+ remote_ip: request.ip,
+ request_method: request.request_method,
+ path: request.fullpath,
+ login: user.username
+ )
+
+ gl_auth.find_for_git_client(user.username, 'wrong_password', project: nil, request: request)
+ end
end
end
end
context 'build token' do
- subject { gl_auth.find_for_git_client(username, build.token, project: project, ip: 'ip') }
+ subject { gl_auth.find_for_git_client(username, build.token, project: project, request: request) }
let(:username) { 'gitlab-ci-token' }
@@ -344,20 +369,20 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching, feature_cate
project.create_drone_ci_integration(active: true)
project.drone_ci_integration.update!(token: 'token', drone_url: generate(:url))
- expect(gl_auth.find_for_git_client('drone-ci-token', 'token', project: project, ip: 'ip')).to have_attributes(actor: nil, project: project, type: :ci, authentication_abilities: described_class.build_authentication_abilities)
+ expect(gl_auth.find_for_git_client('drone-ci-token', 'token', project: project, request: request)).to have_attributes(actor: nil, project: project, type: :ci, authentication_abilities: described_class.build_authentication_abilities)
end
it 'recognizes master passwords' do
user = create(:user)
- expect(gl_auth.find_for_git_client(user.username, user.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, user.password, project: nil, request: request)).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) }
def operation
- expect(gl_auth.find_for_git_client(user.username, user.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, user.password, project: nil, request: request)).to have_attributes(actor: user, project: nil, type: :gitlab_or_ldap, authentication_abilities: described_class.full_authentication_abilities)
end
end
@@ -366,14 +391,14 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching, feature_cate
user = create(:user)
token = Gitlab::LfsToken.new(user).token
- expect(gl_auth.find_for_git_client(user.username, token, project: nil, ip: 'ip')).to have_attributes(actor: user, project: nil, type: :lfs_token, authentication_abilities: described_class.read_write_project_authentication_abilities)
+ expect(gl_auth.find_for_git_client(user.username, token, project: nil, request: request)).to have_attributes(actor: user, project: nil, type: :lfs_token, authentication_abilities: described_class.read_write_project_authentication_abilities)
end
it 'recognizes deploy key lfs tokens' do
key = create(:deploy_key)
token = Gitlab::LfsToken.new(key).token
- expect(gl_auth.find_for_git_client("lfs+deploy-key-#{key.id}", token, project: nil, ip: 'ip')).to have_attributes(actor: key, project: nil, type: :lfs_deploy_token, authentication_abilities: described_class.read_only_authentication_abilities)
+ expect(gl_auth.find_for_git_client("lfs+deploy-key-#{key.id}", token, project: nil, request: request)).to have_attributes(actor: key, project: nil, type: :lfs_deploy_token, authentication_abilities: described_class.read_only_authentication_abilities)
end
it 'does not try password auth before oauth' do
@@ -382,7 +407,7 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching, feature_cate
expect(gl_auth).not_to receive(:find_with_user_password)
- gl_auth.find_for_git_client(user.username, token, project: nil, ip: 'ip')
+ gl_auth.find_for_git_client(user.username, token, project: nil, request: request)
end
it 'grants deploy key write permissions' do
@@ -390,14 +415,14 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching, feature_cate
create(:deploy_keys_project, :write_access, deploy_key: key, project: project)
token = Gitlab::LfsToken.new(key).token
- expect(gl_auth.find_for_git_client("lfs+deploy-key-#{key.id}", token, project: project, ip: 'ip')).to have_attributes(actor: key, project: nil, type: :lfs_deploy_token, authentication_abilities: described_class.read_write_authentication_abilities)
+ expect(gl_auth.find_for_git_client("lfs+deploy-key-#{key.id}", token, project: project, request: request)).to have_attributes(actor: key, project: nil, type: :lfs_deploy_token, authentication_abilities: described_class.read_write_authentication_abilities)
end
it 'does not grant deploy key write permissions' do
key = create(:deploy_key)
token = Gitlab::LfsToken.new(key).token
- expect(gl_auth.find_for_git_client("lfs+deploy-key-#{key.id}", token, project: project, ip: 'ip')).to have_attributes(actor: key, project: nil, type: :lfs_deploy_token, authentication_abilities: described_class.read_only_authentication_abilities)
+ expect(gl_auth.find_for_git_client("lfs+deploy-key-#{key.id}", token, project: project, request: request)).to have_attributes(actor: key, project: nil, type: :lfs_deploy_token, authentication_abilities: described_class.read_only_authentication_abilities)
end
end
@@ -409,7 +434,7 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching, feature_cate
it 'fails' do
access_token = Doorkeeper::AccessToken.create!(application_id: application.id, resource_owner_id: user.id, scopes: 'api')
- expect(gl_auth.find_for_git_client("oauth2", access_token.token, project: nil, ip: 'ip'))
+ expect(gl_auth.find_for_git_client("oauth2", access_token.token, project: nil, request: request))
.to have_attributes(auth_failure)
end
end
@@ -436,7 +461,7 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching, feature_cate
it 'authenticates with correct abilities' do
access_token = Doorkeeper::AccessToken.create!(application_id: application.id, resource_owner_id: user.id, scopes: scopes)
- expect(gl_auth.find_for_git_client("oauth2", access_token.token, project: nil, ip: 'ip'))
+ expect(gl_auth.find_for_git_client("oauth2", access_token.token, project: nil, request: request))
.to have_attributes(actor: user, project: nil, type: :oauth, authentication_abilities: abilities)
end
end
@@ -447,7 +472,7 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching, feature_cate
expect(gl_auth).not_to receive(:find_with_user_password)
- gl_auth.find_for_git_client("oauth2", access_token.token, project: nil, ip: 'ip')
+ gl_auth.find_for_git_client("oauth2", access_token.token, project: nil, request: request)
end
context 'blocked user' do
@@ -513,7 +538,7 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching, feature_cate
impersonation_token = create(:personal_access_token, :impersonation, scopes: ['api'])
- expect(gl_auth.find_for_git_client('', impersonation_token.token, project: nil, ip: 'ip'))
+ expect(gl_auth.find_for_git_client('', impersonation_token.token, project: nil, request: request))
.to have_attributes(auth_failure)
end
@@ -536,7 +561,7 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching, feature_cate
end
it 'fails if user is blocked' do
- expect(gl_auth.find_for_git_client('', personal_access_token.token, project: nil, ip: 'ip'))
+ expect(gl_auth.find_for_git_client('', personal_access_token.token, project: nil, request: request))
.to have_attributes(auth_failure)
end
end
@@ -544,19 +569,19 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching, feature_cate
context 'when using a resource access token' do
shared_examples 'with a valid access token' do
it 'successfully authenticates the project bot' do
- expect(gl_auth.find_for_git_client(project_bot_user.username, access_token.token, project: project, ip: 'ip'))
+ expect(gl_auth.find_for_git_client(project_bot_user.username, access_token.token, project: project, request: request))
.to have_attributes(actor: project_bot_user, project: nil, type: :personal_access_token, authentication_abilities: described_class.full_authentication_abilities)
end
it 'successfully authenticates the project bot with a nil project' do
- expect(gl_auth.find_for_git_client(project_bot_user.username, access_token.token, project: nil, ip: 'ip'))
+ expect(gl_auth.find_for_git_client(project_bot_user.username, access_token.token, project: nil, request: request))
.to have_attributes(actor: project_bot_user, project: nil, type: :personal_access_token, authentication_abilities: described_class.full_authentication_abilities)
end
end
shared_examples 'with an invalid access token' do
it 'fails for a non-member' do
- expect(gl_auth.find_for_git_client(project_bot_user.username, access_token.token, project: project, ip: 'ip'))
+ expect(gl_auth.find_for_git_client(project_bot_user.username, access_token.token, project: project, request: request))
.to have_attributes(auth_failure)
end
@@ -566,7 +591,7 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching, feature_cate
end
it 'fails for a blocked project bot' do
- expect(gl_auth.find_for_git_client(project_bot_user.username, access_token.token, project: project, ip: 'ip'))
+ expect(gl_auth.find_for_git_client(project_bot_user.username, access_token.token, project: project, request: request))
.to have_attributes(auth_failure)
end
end
@@ -637,7 +662,7 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching, feature_cate
it 'updates last_used_at column if token is valid' do
personal_access_token = create(:personal_access_token, scopes: ['write_repository'])
- expect { gl_auth.find_for_git_client('', personal_access_token.token, project: nil, ip: 'ip') }.to change { personal_access_token.reload.last_used_at }
+ expect { gl_auth.find_for_git_client('', personal_access_token.token, project: nil, request: request) }.to change { personal_access_token.reload.last_used_at }
end
end
@@ -649,7 +674,7 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching, feature_cate
username: 'normal_user'
)
- expect(gl_auth.find_for_git_client(user.username, user.password, project: nil, ip: 'ip'))
+ expect(gl_auth.find_for_git_client(user.username, user.password, project: nil, request: request))
.to have_attributes(auth_failure)
end
@@ -665,14 +690,14 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching, feature_cate
it 'fails if grace period expired' do
stub_application_setting(two_factor_grace_period: 0)
- expect { gl_auth.find_for_git_client(user.username, user.password, project: nil, ip: 'ip') }
+ expect { gl_auth.find_for_git_client(user.username, user.password, project: nil, request: request) }
.to raise_error(Gitlab::Auth::MissingPersonalAccessTokenError)
end
it 'goes through if grace period is not expired yet' do
stub_application_setting(two_factor_grace_period: 72)
- expect(gl_auth.find_for_git_client(user.username, user.password, project: nil, ip: 'ip'))
+ expect(gl_auth.find_for_git_client(user.username, user.password, project: nil, request: request))
.to have_attributes(actor: user, project: nil, type: :gitlab_or_ldap, authentication_abilities: described_class.full_authentication_abilities)
end
end
@@ -683,7 +708,7 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching, feature_cate
end
it 'fails' do
- expect { gl_auth.find_for_git_client(user.username, user.password, project: nil, ip: 'ip') }
+ expect { gl_auth.find_for_git_client(user.username, user.password, project: nil, request: request) }
.to raise_error(Gitlab::Auth::MissingPersonalAccessTokenError)
end
end
@@ -694,7 +719,7 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching, feature_cate
username: 'normal_user'
)
- expect(gl_auth.find_for_git_client(user.username, user.password, project: nil, ip: 'ip'))
+ expect(gl_auth.find_for_git_client(user.username, user.password, project: nil, request: request))
.to have_attributes(actor: user, project: nil, type: :gitlab_or_ldap, authentication_abilities: described_class.full_authentication_abilities)
end
@@ -704,7 +729,7 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching, feature_cate
username: 'oauth2'
)
- expect(gl_auth.find_for_git_client(user.username, user.password, project: nil, ip: 'ip'))
+ expect(gl_auth.find_for_git_client(user.username, user.password, project: nil, request: request))
.to have_attributes(actor: user, project: nil, type: :gitlab_or_ldap, authentication_abilities: described_class.full_authentication_abilities)
end
end
@@ -712,34 +737,34 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching, feature_cate
it 'returns double nil for invalid credentials' do
login = 'foo'
- expect(gl_auth.find_for_git_client(login, 'bar', project: nil, ip: 'ip')).to have_attributes(auth_failure)
+ expect(gl_auth.find_for_git_client(login, 'bar', project: nil, request: request)).to have_attributes(auth_failure)
end
it 'throws an error suggesting user create a PAT when internal auth is disabled' do
allow_any_instance_of(ApplicationSetting).to receive(:password_authentication_enabled_for_git?) { false }
- expect { gl_auth.find_for_git_client('foo', 'bar', project: nil, ip: 'ip') }.to raise_error(Gitlab::Auth::MissingPersonalAccessTokenError)
+ expect { gl_auth.find_for_git_client('foo', 'bar', project: nil, request: request) }.to raise_error(Gitlab::Auth::MissingPersonalAccessTokenError)
end
context 'while using deploy tokens' do
shared_examples 'registry token scope' do
it 'fails when login is not valid' do
- expect(gl_auth.find_for_git_client('random_login', deploy_token.token, project: project, ip: 'ip'))
+ expect(gl_auth.find_for_git_client('random_login', deploy_token.token, project: project, request: request))
.to have_attributes(auth_failure)
end
it 'fails when token is not valid' do
- expect(gl_auth.find_for_git_client(login, '123123', project: project, ip: 'ip'))
+ expect(gl_auth.find_for_git_client(login, '123123', project: project, request: request))
.to have_attributes(auth_failure)
end
it 'fails if token is nil' do
- expect(gl_auth.find_for_git_client(login, nil, project: nil, ip: 'ip'))
+ expect(gl_auth.find_for_git_client(login, nil, project: nil, request: request))
.to have_attributes(auth_failure)
end
it 'fails if token is not related to project' do
- expect(gl_auth.find_for_git_client(login, 'abcdef', project: nil, ip: 'ip'))
+ expect(gl_auth.find_for_git_client(login, 'abcdef', project: nil, request: request))
.to have_attributes(auth_failure)
end
@@ -747,7 +772,7 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching, feature_cate
deploy_token.revoke!
expect(deploy_token.revoked?).to be_truthy
- expect(gl_auth.find_for_git_client('deploy-token', deploy_token.token, project: nil, ip: 'ip'))
+ expect(gl_auth.find_for_git_client('deploy-token', deploy_token.token, project: nil, request: request))
.to have_attributes(auth_failure)
end
end
@@ -759,7 +784,7 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching, feature_cate
end
it 'fails when login and token are valid' do
- expect(gl_auth.find_for_git_client(login, deploy_token.token, project: nil, ip: 'ip'))
+ expect(gl_auth.find_for_git_client(login, deploy_token.token, project: nil, request: request))
.to have_attributes(auth_failure)
end
end
@@ -768,7 +793,7 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching, feature_cate
let(:project) { create(:project, :repository_disabled) }
it 'fails when login and token are valid' do
- expect(gl_auth.find_for_git_client(login, deploy_token.token, project: project, ip: 'ip'))
+ expect(gl_auth.find_for_git_client(login, deploy_token.token, project: project, request: request))
.to have_attributes(auth_failure)
end
end
@@ -782,14 +807,14 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching, feature_cate
it 'succeeds for the token' do
auth_success = { actor: deploy_token, project: project, type: :deploy_token, authentication_abilities: [:download_code] }
- expect(gl_auth.find_for_git_client(username, deploy_token.token, project: project, ip: 'ip'))
+ expect(gl_auth.find_for_git_client(username, deploy_token.token, project: project, request: request))
.to have_attributes(auth_success)
end
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, user.password, project: project, ip: 'ip'))
+ expect(gl_auth.find_for_git_client(username, user.password, project: project, request: request))
.to have_attributes(auth_success)
end
end
@@ -801,12 +826,12 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching, feature_cate
let(:auth_success) { { actor: read_repository, project: project, type: :deploy_token, authentication_abilities: [:download_code] } }
it 'succeeds for the right token' do
- expect(gl_auth.find_for_git_client('deployer', read_repository.token, project: project, ip: 'ip'))
+ expect(gl_auth.find_for_git_client('deployer', read_repository.token, project: project, request: request))
.to have_attributes(auth_success)
end
it 'fails for the wrong token' do
- expect(gl_auth.find_for_git_client('deployer', read_registry.token, project: project, ip: 'ip'))
+ expect(gl_auth.find_for_git_client('deployer', read_registry.token, project: project, request: request))
.not_to have_attributes(auth_success)
end
end
@@ -819,12 +844,12 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching, feature_cate
let(:auth_success) { { actor: read_repository, project: other_project, type: :deploy_token, authentication_abilities: [:download_code] } }
it 'succeeds for the right token' do
- expect(gl_auth.find_for_git_client('deployer', read_repository.token, project: other_project, ip: 'ip'))
+ expect(gl_auth.find_for_git_client('deployer', read_repository.token, project: other_project, request: request))
.to have_attributes(auth_success)
end
it 'fails for the wrong token' do
- expect(gl_auth.find_for_git_client('deployer', read_registry.token, project: other_project, ip: 'ip'))
+ expect(gl_auth.find_for_git_client('deployer', read_registry.token, project: other_project, request: request))
.not_to have_attributes(auth_success)
end
end
@@ -837,7 +862,7 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching, feature_cate
it 'succeeds when login and token are valid' do
auth_success = { actor: deploy_token, project: project, type: :deploy_token, authentication_abilities: [:download_code] }
- expect(gl_auth.find_for_git_client(login, deploy_token.token, project: project, ip: 'ip'))
+ expect(gl_auth.find_for_git_client(login, deploy_token.token, project: project, request: request))
.to have_attributes(auth_success)
end
@@ -845,34 +870,34 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching, feature_cate
deploy_token = create(:deploy_token, username: 'deployer', read_registry: false, projects: [project])
auth_success = { actor: deploy_token, project: project, type: :deploy_token, authentication_abilities: [:download_code] }
- expect(gl_auth.find_for_git_client('deployer', deploy_token.token, project: project, ip: 'ip'))
+ expect(gl_auth.find_for_git_client('deployer', deploy_token.token, project: project, request: request))
.to have_attributes(auth_success)
end
it 'does not attempt to rate limit unique IPs for a deploy token' do
expect(Gitlab::Auth::UniqueIpsLimiter).not_to receive(:limit_user!)
- gl_auth.find_for_git_client(login, deploy_token.token, project: project, ip: 'ip')
+ gl_auth.find_for_git_client(login, deploy_token.token, project: project, request: request)
end
it 'fails when login is not valid' do
- expect(gl_auth.find_for_git_client('random_login', deploy_token.token, project: project, ip: 'ip'))
+ expect(gl_auth.find_for_git_client('random_login', deploy_token.token, project: project, request: request))
.to have_attributes(auth_failure)
end
it 'fails when token is not valid' do
- expect(gl_auth.find_for_git_client(login, '123123', project: project, ip: 'ip'))
+ expect(gl_auth.find_for_git_client(login, '123123', project: project, request: request))
.to have_attributes(auth_failure)
end
it 'fails if token is nil' do
- expect(gl_auth.find_for_git_client(login, nil, project: project, ip: 'ip'))
+ expect(gl_auth.find_for_git_client(login, nil, project: project, request: request))
.to have_attributes(auth_failure)
end
it 'fails if token is not related to project' do
another_deploy_token = create(:deploy_token)
- expect(gl_auth.find_for_git_client(another_deploy_token.username, another_deploy_token.token, project: project, ip: 'ip'))
+ expect(gl_auth.find_for_git_client(another_deploy_token.username, another_deploy_token.token, project: project, request: request))
.to have_attributes(auth_failure)
end
@@ -880,7 +905,7 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching, feature_cate
deploy_token.revoke!
expect(deploy_token.revoked?).to be_truthy
- expect(gl_auth.find_for_git_client('deploy-token', deploy_token.token, project: project, ip: 'ip'))
+ expect(gl_auth.find_for_git_client('deploy-token', deploy_token.token, project: project, request: request))
.to have_attributes(auth_failure)
end
end
@@ -890,7 +915,7 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching, feature_cate
let(:deploy_token) { create(:deploy_token, :group, read_repository: true, groups: [project_with_group.group]) }
let(:login) { deploy_token.username }
- subject { gl_auth.find_for_git_client(login, deploy_token.token, project: project_with_group, ip: 'ip') }
+ subject { gl_auth.find_for_git_client(login, deploy_token.token, project: project_with_group, request: request) }
it 'succeeds when login and a group deploy token are valid' do
auth_success = { actor: deploy_token, project: project_with_group, type: :deploy_token, authentication_abilities: [:download_code, :read_container_image] }
@@ -901,7 +926,7 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching, feature_cate
it 'fails if token is not related to group' do
another_deploy_token = create(:deploy_token, :group, read_repository: true)
- expect(gl_auth.find_for_git_client(another_deploy_token.username, another_deploy_token.token, project: project_with_group, ip: 'ip'))
+ expect(gl_auth.find_for_git_client(another_deploy_token.username, another_deploy_token.token, project: project_with_group, request: request))
.to have_attributes(auth_failure)
end
end
@@ -918,7 +943,7 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching, feature_cate
it 'succeeds when login and a project token are valid' do
auth_success = { actor: deploy_token, project: project, type: :deploy_token, authentication_abilities: [:read_container_image] }
- expect(gl_auth.find_for_git_client(login, deploy_token.token, project: project, ip: 'ip'))
+ expect(gl_auth.find_for_git_client(login, deploy_token.token, project: project, request: request))
.to have_attributes(auth_success)
end
@@ -940,7 +965,7 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching, feature_cate
it 'succeeds when login and a project token are valid' do
auth_success = { actor: deploy_token, project: project, type: :deploy_token, authentication_abilities: [:create_container_image] }
- expect(gl_auth.find_for_git_client(login, deploy_token.token, project: project, ip: 'ip'))
+ expect(gl_auth.find_for_git_client(login, deploy_token.token, project: project, request: request))
.to have_attributes(auth_success)
end
@@ -953,7 +978,7 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching, feature_cate
end
describe '#build_access_token_check' do
- subject { gl_auth.find_for_git_client('gitlab-ci-token', build.token, project: build.project, ip: '1.2.3.4') }
+ subject { gl_auth.find_for_git_client('gitlab-ci-token', build.token, project: build.project, request: request) }
let_it_be(:user) { create(:user) }
@@ -1143,7 +1168,7 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching, feature_cate
private
def expect_results_with_abilities(personal_access_token, abilities, success = true)
- expect(gl_auth.find_for_git_client('', personal_access_token&.token, project: nil, ip: 'ip'))
+ expect(gl_auth.find_for_git_client('', personal_access_token&.token, project: nil, request: request))
.to have_attributes(actor: personal_access_token&.user, project: nil, type: personal_access_token.nil? ? nil : :personal_access_token, authentication_abilities: abilities)
end
end
diff --git a/spec/lib/gitlab/background_migration/backfill_packages_tags_project_id_spec.rb b/spec/lib/gitlab/background_migration/backfill_packages_tags_project_id_spec.rb
new file mode 100644
index 00000000000..423d9fe76ac
--- /dev/null
+++ b/spec/lib/gitlab/background_migration/backfill_packages_tags_project_id_spec.rb
@@ -0,0 +1,42 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::BackgroundMigration::BackfillPackagesTagsProjectId,
+ feature_category: :package_registry,
+ schema: 20231030051837 do # schema before we introduced the invalid not-null constraint
+ let!(:tags_without_project_id) do
+ (0...13).map do |i|
+ namespace = table(:namespaces).create!(name: 'my namespace', path: 'my-namespace')
+ project = table(:projects).create!(name: 'my project', path: 'my-project', namespace_id: namespace.id,
+ project_namespace_id: namespace.id)
+ package = table(:packages_packages).create!(project_id: project.id, created_at: Time.current,
+ updated_at: Time.current, name: "Package #{i}", package_type: 1, status: 1)
+ table(:packages_tags).create!(package_id: package.id, name: "Tag #{i}", created_at: Time.current,
+ updated_at: Time.current, project_id: nil)
+ end
+ end
+
+ let!(:starting_id) { table(:packages_tags).pluck(:id).min }
+ let!(:end_id) { table(:packages_tags).pluck(:id).max }
+
+ let!(:migration) do
+ described_class.new(
+ start_id: starting_id,
+ end_id: end_id,
+ batch_table: :packages_tags,
+ batch_column: :id,
+ sub_batch_size: 10,
+ pause_ms: 2,
+ connection: ::ApplicationRecord.connection
+ )
+ end
+
+ it 'backfills the missing project_id for the batch' do
+ expect do
+ migration.perform
+ end.to change { table(:packages_tags).where(project_id: nil).count }
+ .from(13)
+ .to(0)
+ end
+end
diff --git a/spec/lib/gitlab/background_migration/batched_migration_job_spec.rb b/spec/lib/gitlab/background_migration/batched_migration_job_spec.rb
index 781bf93dd85..da24e9b7978 100644
--- a/spec/lib/gitlab/background_migration/batched_migration_job_spec.rb
+++ b/spec/lib/gitlab/background_migration/batched_migration_job_spec.rb
@@ -2,22 +2,22 @@
require 'spec_helper'
-RSpec.describe Gitlab::BackgroundMigration::BatchedMigrationJob do
+RSpec.describe Gitlab::BackgroundMigration::BatchedMigrationJob, feature_category: :database do
let(:connection) { Gitlab::Database.database_base_models[:main].connection }
describe '.generic_instance' do
it 'defines generic instance with only some of the attributes set' do
generic_instance = described_class.generic_instance(
batch_table: 'projects', batch_column: 'id',
- job_arguments: %w(x y), connection: connection
+ job_arguments: %w[x y], connection: connection
)
expect(generic_instance.send(:batch_table)).to eq('projects')
expect(generic_instance.send(:batch_column)).to eq('id')
- expect(generic_instance.instance_variable_get(:@job_arguments)).to eq(%w(x y))
+ expect(generic_instance.instance_variable_get(:@job_arguments)).to eq(%w[x y])
expect(generic_instance.send(:connection)).to eq(connection)
- %i(start_id end_id sub_batch_size pause_ms).each do |attr|
+ %i[start_id end_id sub_batch_size pause_ms].each do |attr|
expect(generic_instance.send(attr)).to eq(0)
end
end
@@ -31,13 +31,16 @@ RSpec.describe Gitlab::BackgroundMigration::BatchedMigrationJob do
end
subject(:job_instance) do
- job_class.new(start_id: 1, end_id: 10,
- batch_table: '_test_table',
- batch_column: 'id',
- sub_batch_size: 2,
- pause_ms: 1000,
- job_arguments: %w(a b),
- connection: connection)
+ job_class.new(
+ start_id: 1,
+ end_id: 10,
+ batch_table: '_test_table',
+ batch_column: 'id',
+ sub_batch_size: 2,
+ pause_ms: 1000,
+ job_arguments: %w[a b],
+ connection: connection
+ )
end
it 'defines methods' do
@@ -61,13 +64,16 @@ RSpec.describe Gitlab::BackgroundMigration::BatchedMigrationJob do
subject(:perform_job) { job_instance.perform }
let(:job_instance) do
- job_class.new(start_id: 1, end_id: 10,
- batch_table: '_test_table',
- batch_column: 'id',
- sub_batch_size: 2,
- pause_ms: 1000,
- job_arguments: %w(a b),
- connection: connection)
+ job_class.new(
+ start_id: 1,
+ end_id: 10,
+ batch_table: '_test_table',
+ batch_column: 'id',
+ sub_batch_size: 2,
+ pause_ms: 1000,
+ job_arguments: %w[a b],
+ connection: connection
+ )
end
let(:job_class) do
@@ -124,13 +130,16 @@ RSpec.describe Gitlab::BackgroundMigration::BatchedMigrationJob do
describe '.scope_to' do
subject(:job_instance) do
- job_class.new(start_id: 1, end_id: 10,
- batch_table: '_test_table',
- batch_column: 'id',
- sub_batch_size: 2,
- pause_ms: 1000,
- job_arguments: %w(a b),
- connection: connection)
+ job_class.new(
+ start_id: 1,
+ end_id: 10,
+ batch_table: '_test_table',
+ batch_column: 'id',
+ sub_batch_size: 2,
+ pause_ms: 1000,
+ job_arguments: %w[a b],
+ connection: connection
+ )
end
context 'when additional scoping is defined' do
@@ -203,12 +212,15 @@ RSpec.describe Gitlab::BackgroundMigration::BatchedMigrationJob do
let(:job_class) { Class.new(described_class) }
let(:job_instance) do
- job_class.new(start_id: 1, end_id: 10,
- batch_table: '_test_table',
- batch_column: 'id',
- sub_batch_size: 2,
- pause_ms: 1000,
- connection: connection)
+ job_class.new(
+ start_id: 1,
+ end_id: 10,
+ batch_table: '_test_table',
+ batch_column: 'id',
+ sub_batch_size: 2,
+ pause_ms: 1000,
+ connection: connection
+ )
end
subject(:perform_job) { job_instance.perform }
@@ -313,9 +325,16 @@ RSpec.describe Gitlab::BackgroundMigration::BatchedMigrationJob do
end
let(:job_instance) do
- job_class.new(start_id: 1, end_id: 10, batch_table: '_test_table', batch_column: 'id',
- sub_batch_size: 2, pause_ms: 1000, connection: connection,
- sub_batch_exception: StandardError)
+ job_class.new(
+ start_id: 1,
+ end_id: 10,
+ batch_table: '_test_table',
+ batch_column: 'id',
+ sub_batch_size: 2,
+ pause_ms: 1000,
+ connection: connection,
+ sub_batch_exception: StandardError
+ )
end
it 'raises the expected error type' do
@@ -336,13 +355,15 @@ RSpec.describe Gitlab::BackgroundMigration::BatchedMigrationJob do
context 'when the subclass uses distinct each batch' do
let(:job_instance) do
- job_class.new(start_id: 1,
- end_id: 100,
- batch_table: '_test_table',
- batch_column: 'from_column',
- sub_batch_size: 2,
- pause_ms: 10,
- connection: connection)
+ job_class.new(
+ start_id: 1,
+ end_id: 100,
+ batch_table: '_test_table',
+ batch_column: 'from_column',
+ sub_batch_size: 2,
+ pause_ms: 10,
+ connection: connection
+ )
end
let(:job_class) do
diff --git a/spec/lib/gitlab/background_migration/copy_column_using_background_migration_job_spec.rb b/spec/lib/gitlab/background_migration/copy_column_using_background_migration_job_spec.rb
index 9c33100a0b3..a827116a900 100644
--- a/spec/lib/gitlab/background_migration/copy_column_using_background_migration_job_spec.rb
+++ b/spec/lib/gitlab/background_migration/copy_column_using_background_migration_job_spec.rb
@@ -16,16 +16,18 @@ RSpec.describe Gitlab::BackgroundMigration::CopyColumnUsingBackgroundMigrationJo
ActiveRecord::Migration.new.extend(Gitlab::Database::MigrationHelpers)
end
- let(:job_arguments) { %w(name name_convert_to_text) }
+ let(:job_arguments) { %w[name name_convert_to_text] }
let(:copy_job) do
- described_class.new(start_id: 12,
- end_id: 20,
- batch_table: table_name,
- batch_column: 'id',
- sub_batch_size: sub_batch_size,
- pause_ms: pause_ms,
- job_arguments: job_arguments,
- connection: connection)
+ described_class.new(
+ start_id: 12,
+ end_id: 20,
+ batch_table: table_name,
+ batch_column: 'id',
+ sub_batch_size: sub_batch_size,
+ pause_ms: pause_ms,
+ job_arguments: job_arguments,
+ connection: connection
+ )
end
before do
@@ -82,7 +84,7 @@ RSpec.describe Gitlab::BackgroundMigration::CopyColumnUsingBackgroundMigrationJo
end
context 'columns with NULLs' do
- let(:job_arguments) { %w(name name_convert_to_text) }
+ let(:job_arguments) { %w[name name_convert_to_text] }
it 'copies all in range' do
expect { copy_job.perform }
diff --git a/spec/lib/gitlab/background_migration/delete_invalid_protected_branch_merge_access_levels_spec.rb b/spec/lib/gitlab/background_migration/delete_invalid_protected_branch_merge_access_levels_spec.rb
new file mode 100644
index 00000000000..1e5b9d30436
--- /dev/null
+++ b/spec/lib/gitlab/background_migration/delete_invalid_protected_branch_merge_access_levels_spec.rb
@@ -0,0 +1,76 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::BackgroundMigration::DeleteInvalidProtectedBranchMergeAccessLevels,
+ feature_category: :source_code_management do
+ let(:projects_table) { table(:projects) }
+ let(:protected_branches_table) { table(:protected_branches) }
+ let(:namespaces_table) { table(:namespaces) }
+ let(:protected_branch_merge_access_levels_table) { table(:protected_branch_merge_access_levels) }
+ let(:project_group_links_table) { table(:project_group_links) }
+ let(:users_table) { table(:users) }
+
+ let(:user1) { users_table.create!(name: 'user1', email: 'user1@example.com', projects_limit: 5) }
+
+ let(:project_group) { namespaces_table.create!(name: 'group-1', path: 'group-1', type: 'Group') }
+ let(:project_namespace) { namespaces_table.create!(name: 'namespace', path: 'namespace-path-2', type: 'Project') }
+ let!(:project_1) do
+ projects_table
+ .create!(
+ name: 'project1',
+ path: 'path1',
+ namespace_id: project_group.id,
+ project_namespace_id: project_namespace.id,
+ visibility_level: 0
+ )
+ end
+
+ subject(:perform_migration) do
+ described_class.new(start_id: protected_branch_merge_access_levels_table.minimum(:id),
+ end_id: protected_branch_merge_access_levels_table.maximum(:id),
+ batch_table: :protected_branch_merge_access_levels,
+ batch_column: :id,
+ sub_batch_size: 1,
+ pause_ms: 0,
+ connection: ApplicationRecord.connection)
+ .perform
+ end
+
+ context 'when there are merge access levels' do
+ let(:protected_branch1) { protected_branches_table.create!(project_id: project_1.id, name: 'name') }
+ let!(:merge_access_level_for_user) do
+ protected_branch_merge_access_levels_table.create!(
+ protected_branch_id: protected_branch1.id,
+ user_id: user1.id
+ )
+ end
+
+ let(:invited_group) { namespaces_table.create!(name: 'group-2', path: 'group-2', type: 'Group') }
+ let!(:invited_group_link) do
+ project_group_links_table.create!(project_id: project_1.id, group_id: invited_group.id)
+ end
+
+ let!(:merge_access_level_with_linked_group) do
+ protected_branch_merge_access_levels_table.create!(
+ protected_branch_id: protected_branch1.id,
+ group_id: invited_group.id
+ )
+ end
+
+ let!(:merge_access_level_with_unlinked_group) do
+ protected_branch_merge_access_levels_table.create!(
+ protected_branch_id: protected_branch1.id,
+ group_id: project_group.id
+ )
+ end
+
+ it 'deletes merge access levels with groups that do not have project_group_links to the project' do
+ expect { subject }.to change { protected_branch_merge_access_levels_table.count }.from(3).to(2)
+ expect(protected_branch_merge_access_levels_table.all).to contain_exactly(
+ merge_access_level_with_linked_group,
+ merge_access_level_for_user
+ )
+ end
+ end
+end
diff --git a/spec/lib/gitlab/background_migration/delete_invalid_protected_branch_push_access_levels_spec.rb b/spec/lib/gitlab/background_migration/delete_invalid_protected_branch_push_access_levels_spec.rb
new file mode 100644
index 00000000000..62201831dd1
--- /dev/null
+++ b/spec/lib/gitlab/background_migration/delete_invalid_protected_branch_push_access_levels_spec.rb
@@ -0,0 +1,76 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::BackgroundMigration::DeleteInvalidProtectedBranchPushAccessLevels,
+ feature_category: :source_code_management do
+ let(:projects_table) { table(:projects) }
+ let(:protected_branches_table) { table(:protected_branches) }
+ let(:namespaces_table) { table(:namespaces) }
+ let(:protected_branch_push_access_levels_table) { table(:protected_branch_push_access_levels) }
+ let(:project_group_links_table) { table(:project_group_links) }
+ let(:users_table) { table(:users) }
+
+ let(:user1) { users_table.create!(name: 'user1', email: 'user1@example.com', projects_limit: 5) }
+
+ let(:project_group) { namespaces_table.create!(name: 'group-1', path: 'group-1', type: 'Group') }
+ let(:project_namespace) { namespaces_table.create!(name: 'namespace', path: 'namespace-path-2', type: 'Project') }
+ let!(:project_1) do
+ projects_table
+ .create!(
+ name: 'project1',
+ path: 'path1',
+ namespace_id: project_group.id,
+ project_namespace_id: project_namespace.id,
+ visibility_level: 0
+ )
+ end
+
+ subject(:perform_migration) do
+ described_class.new(start_id: protected_branch_push_access_levels_table.minimum(:id),
+ end_id: protected_branch_push_access_levels_table.maximum(:id),
+ batch_table: :protected_branch_push_access_levels,
+ batch_column: :id,
+ sub_batch_size: 1,
+ pause_ms: 0,
+ connection: ApplicationRecord.connection)
+ .perform
+ end
+
+ context 'when there are push access levels' do
+ let(:protected_branch1) { protected_branches_table.create!(project_id: project_1.id, name: 'name') }
+ let!(:push_access_level_for_user) do
+ protected_branch_push_access_levels_table.create!(
+ protected_branch_id: protected_branch1.id,
+ user_id: user1.id
+ )
+ end
+
+ let(:invited_group) { namespaces_table.create!(name: 'group-2', path: 'group-2', type: 'Group') }
+ let!(:invited_group_link) do
+ project_group_links_table.create!(project_id: project_1.id, group_id: invited_group.id)
+ end
+
+ let!(:push_access_level_with_linked_group) do
+ protected_branch_push_access_levels_table.create!(
+ protected_branch_id: protected_branch1.id,
+ group_id: invited_group.id
+ )
+ end
+
+ let!(:push_access_level_with_unlinked_group) do
+ protected_branch_push_access_levels_table.create!(
+ protected_branch_id: protected_branch1.id,
+ group_id: project_group.id
+ )
+ end
+
+ it 'deletes push access levels with groups that do not have project_group_links to the project' do
+ expect { subject }.to change { protected_branch_push_access_levels_table.count }.from(3).to(2)
+ expect(protected_branch_push_access_levels_table.all).to contain_exactly(
+ push_access_level_with_linked_group,
+ push_access_level_for_user
+ )
+ end
+ end
+end
diff --git a/spec/lib/gitlab/background_migration/delete_invalid_protected_tag_create_access_levels_spec.rb b/spec/lib/gitlab/background_migration/delete_invalid_protected_tag_create_access_levels_spec.rb
new file mode 100644
index 00000000000..fd6cee9e4db
--- /dev/null
+++ b/spec/lib/gitlab/background_migration/delete_invalid_protected_tag_create_access_levels_spec.rb
@@ -0,0 +1,76 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::BackgroundMigration::DeleteInvalidProtectedTagCreateAccessLevels,
+ feature_category: :source_code_management do
+ let(:projects_table) { table(:projects) }
+ let(:protected_tags_table) { table(:protected_tags) }
+ let(:namespaces_table) { table(:namespaces) }
+ let(:protected_tag_create_access_levels_table) { table(:protected_tag_create_access_levels) }
+ let(:project_group_links_table) { table(:project_group_links) }
+ let(:users_table) { table(:users) }
+
+ let(:user1) { users_table.create!(name: 'user1', email: 'user1@example.com', projects_limit: 5) }
+
+ let(:project_group) { namespaces_table.create!(name: 'group-1', path: 'group-1', type: 'Group') }
+ let(:project_namespace) { namespaces_table.create!(name: 'namespace', path: 'namespace-path-2', type: 'Project') }
+ let!(:project_1) do
+ projects_table
+ .create!(
+ name: 'project1',
+ path: 'path1',
+ namespace_id: project_group.id,
+ project_namespace_id: project_namespace.id,
+ visibility_level: 0
+ )
+ end
+
+ subject(:perform_migration) do
+ described_class.new(start_id: protected_tag_create_access_levels_table.minimum(:id),
+ end_id: protected_tag_create_access_levels_table.maximum(:id),
+ batch_table: :protected_tag_create_access_levels,
+ batch_column: :id,
+ sub_batch_size: 1,
+ pause_ms: 0,
+ connection: ApplicationRecord.connection)
+ .perform
+ end
+
+ context 'when there are push access levels' do
+ let(:protected_tag) { protected_tags_table.create!(project_id: project_1.id, name: 'name') }
+ let!(:push_access_level_for_user) do
+ protected_tag_create_access_levels_table.create!(
+ protected_tag_id: protected_tag.id,
+ user_id: user1.id
+ )
+ end
+
+ let(:invited_group) { namespaces_table.create!(name: 'group-2', path: 'group-2', type: 'Group') }
+ let!(:invited_group_link) do
+ project_group_links_table.create!(project_id: project_1.id, group_id: invited_group.id)
+ end
+
+ let!(:push_access_level_with_linked_group) do
+ protected_tag_create_access_levels_table.create!(
+ protected_tag_id: protected_tag.id,
+ group_id: invited_group.id
+ )
+ end
+
+ let!(:push_access_level_with_unlinked_group) do
+ protected_tag_create_access_levels_table.create!(
+ protected_tag_id: protected_tag.id,
+ group_id: project_group.id
+ )
+ end
+
+ it 'deletes push access levels with groups that do not have project_group_links to the project' do
+ expect { subject }.to change { protected_tag_create_access_levels_table.count }.from(3).to(2)
+ expect(protected_tag_create_access_levels_table.all).to contain_exactly(
+ push_access_level_with_linked_group,
+ push_access_level_for_user
+ )
+ end
+ end
+end
diff --git a/spec/lib/gitlab/background_migration/delete_orphaned_operational_vulnerabilities_spec.rb b/spec/lib/gitlab/background_migration/delete_orphaned_operational_vulnerabilities_spec.rb
index c03962c8d21..4a1985eeccd 100644
--- a/spec/lib/gitlab/background_migration/delete_orphaned_operational_vulnerabilities_spec.rb
+++ b/spec/lib/gitlab/background_migration/delete_orphaned_operational_vulnerabilities_spec.rb
@@ -87,13 +87,15 @@ RSpec.describe Gitlab::BackgroundMigration::DeleteOrphanedOperationalVulnerabili
end
subject(:background_migration) do
- described_class.new(start_id: vulnerabilities.minimum(:id),
- end_id: vulnerabilities.maximum(:id),
- batch_table: :vulnerabilities,
- batch_column: :id,
- sub_batch_size: 2,
- pause_ms: 0,
- connection: ActiveRecord::Base.connection)
+ described_class.new(
+ start_id: vulnerabilities.minimum(:id),
+ end_id: vulnerabilities.maximum(:id),
+ batch_table: :vulnerabilities,
+ batch_column: :id,
+ sub_batch_size: 2,
+ pause_ms: 0,
+ connection: ActiveRecord::Base.connection
+ )
end
it 'drops Cluster Image Scanning and Custom Vulnerabilities without any Findings' do
diff --git a/spec/lib/gitlab/background_migration/delete_orphans_approval_merge_request_rules_spec.rb b/spec/lib/gitlab/background_migration/delete_orphans_approval_merge_request_rules_spec.rb
index c5b46d3f57c..1ac4d184912 100644
--- a/spec/lib/gitlab/background_migration/delete_orphans_approval_merge_request_rules_spec.rb
+++ b/spec/lib/gitlab/background_migration/delete_orphans_approval_merge_request_rules_spec.rb
@@ -22,9 +22,12 @@ RSpec.describe Gitlab::BackgroundMigration::DeleteOrphansApprovalMergeRequestRul
let(:namespace_2) { namespaces.create!(name: 'name_2', path: 'path_2') }
let(:security_project) do
- projects
- .create!(name: "security_project", path: "security_project", namespace_id: namespace_2.id,
- project_namespace_id: namespace_2.id)
+ projects.create!(
+ name: "security_project",
+ path: "security_project",
+ namespace_id: namespace_2.id,
+ project_namespace_id: namespace_2.id
+ )
end
let!(:security_orchestration_policy_configuration) do
diff --git a/spec/lib/gitlab/background_migration/delete_orphans_approval_project_rules_spec.rb b/spec/lib/gitlab/background_migration/delete_orphans_approval_project_rules_spec.rb
index 16253255764..23026f76001 100644
--- a/spec/lib/gitlab/background_migration/delete_orphans_approval_project_rules_spec.rb
+++ b/spec/lib/gitlab/background_migration/delete_orphans_approval_project_rules_spec.rb
@@ -22,9 +22,12 @@ RSpec.describe Gitlab::BackgroundMigration::DeleteOrphansApprovalProjectRules do
let(:namespace_2) { namespaces.create!(name: 'name_2', path: 'path_2') }
let(:security_project) do
- projects
- .create!(name: "security_project", path: "security_project", namespace_id: namespace_2.id,
- project_namespace_id: namespace_2.id)
+ projects.create!(
+ name: "security_project",
+ path: "security_project",
+ namespace_id: namespace_2.id,
+ project_namespace_id: namespace_2.id
+ )
end
let!(:security_orchestration_policy_configuration) do
diff --git a/spec/lib/gitlab/background_migration/destroy_invalid_group_members_spec.rb b/spec/lib/gitlab/background_migration/destroy_invalid_group_members_spec.rb
index 76a9ea82c76..4e136808a36 100644
--- a/spec/lib/gitlab/background_migration/destroy_invalid_group_members_spec.rb
+++ b/spec/lib/gitlab/background_migration/destroy_invalid_group_members_spec.rb
@@ -76,13 +76,29 @@ RSpec.describe Gitlab::BackgroundMigration::DestroyInvalidGroupMembers, :migrati
end
def create_invalid_group_member(id:, user_id:)
- members_table.create!(id: id, user_id: user_id, source_id: non_existing_record_id, access_level: Gitlab::Access::MAINTAINER,
- type: "GroupMember", source_type: "Namespace", notification_level: 3, member_namespace_id: nil)
+ members_table.create!(
+ id: id,
+ user_id: user_id,
+ source_id: non_existing_record_id,
+ access_level: Gitlab::Access::MAINTAINER,
+ type: "GroupMember",
+ source_type: "Namespace",
+ notification_level: 3,
+ member_namespace_id: nil
+ )
end
def create_valid_group_member(id:, user_id:, group_id:)
- members_table.create!(id: id, user_id: user_id, source_id: group_id, access_level: Gitlab::Access::MAINTAINER,
- type: "GroupMember", source_type: "Namespace", member_namespace_id: group_id, notification_level: 3)
+ members_table.create!(
+ id: id,
+ user_id: user_id,
+ source_id: group_id,
+ access_level: Gitlab::Access::MAINTAINER,
+ type: "GroupMember",
+ source_type: "Namespace",
+ member_namespace_id: group_id,
+ notification_level: 3
+ )
end
# rubocop: enable Layout/LineLength
# rubocop: enable RSpec/ScatteredLet
diff --git a/spec/lib/gitlab/background_migration/destroy_invalid_members_spec.rb b/spec/lib/gitlab/background_migration/destroy_invalid_members_spec.rb
index 5059ad620aa..e5965d4a1d8 100644
--- a/spec/lib/gitlab/background_migration/destroy_invalid_members_spec.rb
+++ b/spec/lib/gitlab/background_migration/destroy_invalid_members_spec.rb
@@ -33,23 +33,39 @@ RSpec.describe Gitlab::BackgroundMigration::DestroyInvalidMembers, :migration, s
let!(:group1) { namespaces_table.create!(name: 'marvellous group 1', path: 'group-path-1', type: 'Group') }
let!(:group2) { namespaces_table.create!(name: 'outstanding group 2', path: 'group-path-2', type: 'Group') }
let!(:project_namespace1) do
- namespaces_table.create!(name: 'fabulous project', path: 'project-path-1',
- type: 'ProjectNamespace', parent_id: group1.id)
+ namespaces_table.create!(
+ name: 'fabulous project',
+ path: 'project-path-1',
+ type: 'ProjectNamespace',
+ parent_id: group1.id
+ )
end
let!(:project1) do
- projects_table.create!(name: 'fabulous project', path: 'project-path-1',
- project_namespace_id: project_namespace1.id, namespace_id: group1.id)
+ projects_table.create!(
+ name: 'fabulous project',
+ path: 'project-path-1',
+ project_namespace_id: project_namespace1.id,
+ namespace_id: group1.id
+ )
end
let!(:project_namespace2) do
- namespaces_table.create!(name: 'splendiferous project', path: 'project-path-2',
- type: 'ProjectNamespace', parent_id: group1.id)
+ namespaces_table.create!(
+ name: 'splendiferous project',
+ path: 'project-path-2',
+ type: 'ProjectNamespace',
+ parent_id: group1.id
+ )
end
let!(:project2) do
- projects_table.create!(name: 'splendiferous project', path: 'project-path-2',
- project_namespace_id: project_namespace2.id, namespace_id: group1.id)
+ projects_table.create!(
+ name: 'splendiferous project',
+ path: 'project-path-2',
+ project_namespace_id: project_namespace2.id,
+ namespace_id: group1.id
+ )
end
# create valid project member records
@@ -115,27 +131,55 @@ RSpec.describe Gitlab::BackgroundMigration::DestroyInvalidMembers, :migration, s
end
def create_invalid_project_member(id:, user_id:)
- members_table.create!(id: id, user_id: user_id, source_id: non_existing_record_id,
- access_level: Gitlab::Access::MAINTAINER, type: "ProjectMember",
- source_type: "Project", notification_level: 3, member_namespace_id: nil)
+ members_table.create!(
+ id: id,
+ user_id: user_id,
+ source_id: non_existing_record_id,
+ access_level: Gitlab::Access::MAINTAINER,
+ type: "ProjectMember",
+ source_type: "Project",
+ notification_level: 3,
+ member_namespace_id: nil
+ )
end
def create_valid_project_member(id:, user_id:, project:)
- members_table.create!(id: id, user_id: user_id, source_id: project.id,
- access_level: Gitlab::Access::MAINTAINER, type: "ProjectMember", source_type: "Project",
- member_namespace_id: project.project_namespace_id, notification_level: 3)
+ members_table.create!(
+ id: id,
+ user_id: user_id,
+ source_id: project.id,
+ access_level: Gitlab::Access::MAINTAINER,
+ type: "ProjectMember",
+ source_type: "Project",
+ member_namespace_id: project.project_namespace_id,
+ notification_level: 3
+ )
end
def create_invalid_group_member(id:, user_id:)
- members_table.create!(id: id, user_id: user_id, source_id: non_existing_record_id,
- access_level: Gitlab::Access::MAINTAINER, type: "GroupMember",
- source_type: "Namespace", notification_level: 3, member_namespace_id: nil)
+ members_table.create!(
+ id: id,
+ user_id: user_id,
+ source_id: non_existing_record_id,
+ access_level: Gitlab::Access::MAINTAINER,
+ type: "GroupMember",
+ source_type: "Namespace",
+ notification_level: 3,
+ member_namespace_id: nil
+ )
end
def create_valid_group_member(id:, user_id:, group_id:)
- members_table.create!(id: id, user_id: user_id, source_id: group_id,
- access_level: Gitlab::Access::MAINTAINER, type: "GroupMember",
- source_type: "Namespace", member_namespace_id: group_id, notification_level: 3)
+ members_table.create!(
+ id: id,
+ user_id: user_id,
+ source_id: group_id,
+ access_level: Gitlab::Access::MAINTAINER,
+ type: "GroupMember",
+ source_type: "Namespace",
+ member_namespace_id: group_id,
+ notification_level: 3
+ )
end
end
# rubocop: enable RSpec/MultipleMemoizedHelpers
diff --git a/spec/lib/gitlab/background_migration/destroy_invalid_project_members_spec.rb b/spec/lib/gitlab/background_migration/destroy_invalid_project_members_spec.rb
index 029a6adf831..090c31049b4 100644
--- a/spec/lib/gitlab/background_migration/destroy_invalid_project_members_spec.rb
+++ b/spec/lib/gitlab/background_migration/destroy_invalid_project_members_spec.rb
@@ -3,7 +3,6 @@
require 'spec_helper'
RSpec.describe Gitlab::BackgroundMigration::DestroyInvalidProjectMembers, :migration, schema: 20220901035725 do
- # rubocop: disable Layout/LineLength
# rubocop: disable RSpec/ScatteredLet
let!(:migration_attrs) do
{
@@ -36,23 +35,33 @@ RSpec.describe Gitlab::BackgroundMigration::DestroyInvalidProjectMembers, :migra
let!(:group1) { namespaces_table.create!(name: 'marvellous group 1', path: 'group-path-1', type: 'Group') }
let!(:project_namespace1) do
- namespaces_table.create!(name: 'fabulous project', path: 'project-path-1', type: 'ProjectNamespace',
- parent_id: group1.id)
+ namespaces_table.create!(
+ name: 'fabulous project', path: 'project-path-1', type: 'ProjectNamespace', parent_id: group1.id
+ )
end
let!(:project1) do
- projects_table.create!(name: 'fabulous project', path: 'project-path-1', project_namespace_id: project_namespace1.id,
- namespace_id: group1.id)
+ projects_table.create!(
+ name: 'fabulous project',
+ path: 'project-path-1',
+ project_namespace_id: project_namespace1.id,
+ namespace_id: group1.id
+ )
end
let!(:project_namespace2) do
- namespaces_table.create!(name: 'splendiferous project', path: 'project-path-2', type: 'ProjectNamespace',
- parent_id: group1.id)
+ namespaces_table.create!(
+ name: 'splendiferous project', path: 'project-path-2', type: 'ProjectNamespace', parent_id: group1.id
+ )
end
let!(:project2) do
- projects_table.create!(name: 'splendiferous project', path: 'project-path-2', project_namespace_id: project_namespace2.id,
- namespace_id: group1.id)
+ projects_table.create!(
+ name: 'splendiferous project',
+ path: 'project-path-2',
+ project_namespace_id: project_namespace2.id,
+ namespace_id: group1.id
+ )
end
# create project member records, a mix of both valid and invalid
@@ -72,7 +81,8 @@ RSpec.describe Gitlab::BackgroundMigration::DestroyInvalidProjectMembers, :migra
end
expect(queries.count).to eq(4)
- expect(members_table.where(type: 'ProjectMember')).to match_array([project_member2, project_member3, project_member5])
+ expect(members_table.where(type: 'ProjectMember'))
+ .to match_array([project_member2, project_member3, project_member5])
end
it 'tracks timings of queries' do
@@ -82,21 +92,33 @@ RSpec.describe Gitlab::BackgroundMigration::DestroyInvalidProjectMembers, :migra
end
it 'logs IDs of deleted records' do
- expect(Gitlab::AppLogger).to receive(:info).with({ message: 'Removing invalid project member records',
- deleted_count: 3, ids: [project_member1, project_member4, project_member6].map(&:id) })
+ expect(Gitlab::AppLogger).to receive(:info).with({
+ message: 'Removing invalid project member records',
+ deleted_count: 3,
+ ids: [project_member1, project_member4, project_member6].map(&:id)
+ })
perform_migration
end
def create_invalid_project_member(id:, user_id:)
- members_table.create!(id: id, user_id: user_id, source_id: non_existing_record_id, access_level: Gitlab::Access::MAINTAINER,
- type: "ProjectMember", source_type: "Project", notification_level: 3, member_namespace_id: nil)
+ members_table.create!(
+ id: id, user_id: user_id, source_id: non_existing_record_id, access_level: Gitlab::Access::MAINTAINER,
+ type: "ProjectMember", source_type: "Project", notification_level: 3, member_namespace_id: nil
+ )
end
def create_valid_project_member(id:, user_id:, project:)
- members_table.create!(id: id, user_id: user_id, source_id: project.id, access_level: Gitlab::Access::MAINTAINER,
- type: "ProjectMember", source_type: "Project", member_namespace_id: project.project_namespace_id, notification_level: 3)
+ members_table.create!(
+ id: id,
+ user_id: user_id,
+ source_id: project.id,
+ access_level: Gitlab::Access::MAINTAINER,
+ type: "ProjectMember",
+ source_type: "Project",
+ member_namespace_id: project.project_namespace_id,
+ notification_level: 3
+ )
end
- # rubocop: enable Layout/LineLength
# rubocop: enable RSpec/ScatteredLet
end
diff --git a/spec/lib/gitlab/background_migration/disable_legacy_open_source_licence_for_recent_public_projects_spec.rb b/spec/lib/gitlab/background_migration/disable_legacy_open_source_licence_for_recent_public_projects_spec.rb
index 7edba8cf524..740a90e0494 100644
--- a/spec/lib/gitlab/background_migration/disable_legacy_open_source_licence_for_recent_public_projects_spec.rb
+++ b/spec/lib/gitlab/background_migration/disable_legacy_open_source_licence_for_recent_public_projects_spec.rb
@@ -73,14 +73,15 @@ RSpec.describe Gitlab::BackgroundMigration::DisableLegacyOpenSourceLicenceForRec
let(:project_settings_table) { table(:project_settings) }
subject(:perform_migration) do
- described_class.new(start_id: projects_table.minimum(:id),
- end_id: projects_table.maximum(:id),
- batch_table: :projects,
- batch_column: :id,
- sub_batch_size: 2,
- pause_ms: 0,
- connection: ActiveRecord::Base.connection)
- .perform
+ described_class.new(
+ start_id: projects_table.minimum(:id),
+ end_id: projects_table.maximum(:id),
+ batch_table: :projects,
+ batch_column: :id,
+ sub_batch_size: 2,
+ pause_ms: 0,
+ connection: ActiveRecord::Base.connection
+ ).perform
end
before do
@@ -94,7 +95,7 @@ RSpec.describe Gitlab::BackgroundMigration::DisableLegacyOpenSourceLicenceForRec
end
it 'sets `legacy_open_source_license_available` attribute to false for public projects created after threshold time',
- :aggregate_failures do
+ :aggregate_failures do
record = ActiveRecord::QueryRecorder.new do
expect { perform_migration }
.to not_change { migrated_attribute(project_1.id) }.from(true)
diff --git a/spec/lib/gitlab/background_migration/disable_legacy_open_source_license_for_inactive_public_projects_spec.rb b/spec/lib/gitlab/background_migration/disable_legacy_open_source_license_for_inactive_public_projects_spec.rb
index f5a2dc91185..953eb09032f 100644
--- a/spec/lib/gitlab/background_migration/disable_legacy_open_source_license_for_inactive_public_projects_spec.rb
+++ b/spec/lib/gitlab/background_migration/disable_legacy_open_source_license_for_inactive_public_projects_spec.rb
@@ -8,14 +8,15 @@ RSpec.describe Gitlab::BackgroundMigration::DisableLegacyOpenSourceLicenseForIna
let(:project_settings_table) { table(:project_settings) }
subject(:perform_migration) do
- described_class.new(start_id: projects_table.minimum(:id),
- end_id: projects_table.maximum(:id),
- batch_table: :projects,
- batch_column: :id,
- sub_batch_size: 2,
- pause_ms: 0,
- connection: ActiveRecord::Base.connection)
- .perform
+ described_class.new(
+ start_id: projects_table.minimum(:id),
+ end_id: projects_table.maximum(:id),
+ batch_table: :projects,
+ batch_column: :id,
+ sub_batch_size: 2,
+ pause_ms: 0,
+ connection: ActiveRecord::Base.connection
+ ).perform
end
let(:queries) { ActiveRecord::QueryRecorder.new { perform_migration } }
@@ -27,32 +28,28 @@ RSpec.describe Gitlab::BackgroundMigration::DisableLegacyOpenSourceLicenseForIna
let(:project_namespace_5) { namespaces_table.create!(name: 'namespace', path: 'namespace-path-5', type: 'Project') }
let(:project_1) do
- projects_table
- .create!(
+ projects_table.create!(
name: 'proj-1', path: 'path-1', namespace_id: namespace_1.id,
project_namespace_id: project_namespace_2.id, visibility_level: 0
)
end
let(:project_2) do
- projects_table
- .create!(
+ projects_table.create!(
name: 'proj-2', path: 'path-2', namespace_id: namespace_1.id,
project_namespace_id: project_namespace_3.id, visibility_level: 10
)
end
let(:project_3) do
- projects_table
- .create!(
+ projects_table.create!(
name: 'proj-3', path: 'path-3', namespace_id: namespace_1.id,
project_namespace_id: project_namespace_4.id, visibility_level: 20, last_activity_at: '2021-01-01'
)
end
let(:project_4) do
- projects_table
- .create!(
+ projects_table.create!(
name: 'proj-4', path: 'path-4', namespace_id: namespace_1.id,
project_namespace_id: project_namespace_5.id, visibility_level: 20, last_activity_at: '2022-01-01'
)
@@ -66,7 +63,7 @@ RSpec.describe Gitlab::BackgroundMigration::DisableLegacyOpenSourceLicenseForIna
end
it 'sets `legacy_open_source_license_available` attribute to false for inactive, public projects',
- :aggregate_failures do
+ :aggregate_failures do
expect(queries.count).to eq(5)
expect(migrated_attribute(project_1.id)).to be_truthy
diff --git a/spec/lib/gitlab/background_migration/disable_legacy_open_source_license_for_no_issues_no_repo_projects_spec.rb b/spec/lib/gitlab/background_migration/disable_legacy_open_source_license_for_no_issues_no_repo_projects_spec.rb
index d60874c3159..93913a2742b 100644
--- a/spec/lib/gitlab/background_migration/disable_legacy_open_source_license_for_no_issues_no_repo_projects_spec.rb
+++ b/spec/lib/gitlab/background_migration/disable_legacy_open_source_license_for_no_issues_no_repo_projects_spec.rb
@@ -3,8 +3,8 @@
require 'spec_helper'
RSpec.describe Gitlab::BackgroundMigration::DisableLegacyOpenSourceLicenseForNoIssuesNoRepoProjects,
- :migration,
- schema: 20220722084543 do
+ :migration,
+ schema: 20220722084543 do
let(:namespaces_table) { table(:namespaces) }
let(:projects_table) { table(:projects) }
let(:project_settings_table) { table(:project_settings) }
@@ -12,18 +12,19 @@ RSpec.describe Gitlab::BackgroundMigration::DisableLegacyOpenSourceLicenseForNoI
let(:issues_table) { table(:issues) }
subject(:perform_migration) do
- described_class.new(start_id: projects_table.minimum(:id),
- end_id: projects_table.maximum(:id),
- batch_table: :projects,
- batch_column: :id,
- sub_batch_size: 2,
- pause_ms: 0,
- connection: ActiveRecord::Base.connection)
- .perform
+ described_class.new(
+ start_id: projects_table.minimum(:id),
+ end_id: projects_table.maximum(:id),
+ batch_table: :projects,
+ batch_column: :id,
+ sub_batch_size: 2,
+ pause_ms: 0,
+ connection: ActiveRecord::Base.connection
+ ).perform
end
it 'sets `legacy_open_source_license_available` to false only for public projects with no issues and no repo',
- :aggregate_failures do
+ :aggregate_failures do
project_with_no_issues_no_repo = create_legacy_license_public_project('project-with-no-issues-no-repo')
project_with_repo = create_legacy_license_public_project('project-with-repo', repo_size: 1)
project_with_issues = create_legacy_license_public_project('project-with-issues', with_issue: true)
@@ -41,13 +42,13 @@ RSpec.describe Gitlab::BackgroundMigration::DisableLegacyOpenSourceLicenseForNoI
def create_legacy_license_public_project(path, repo_size: 0, with_issue: false)
namespace = namespaces_table.create!(name: "namespace-#{path}", path: "namespace-#{path}")
- project_namespace =
- namespaces_table.create!(name: "-project-namespace-#{path}", path: "project-namespace-#{path}", type: 'Project')
- project = projects_table
- .create!(
- name: path, path: path, namespace_id: namespace.id,
- project_namespace_id: project_namespace.id, visibility_level: 20
- )
+ project_namespace = namespaces_table.create!(
+ name: "-project-namespace-#{path}", path: "project-namespace-#{path}", type: 'Project'
+ )
+ project = projects_table.create!(
+ name: path, path: path, namespace_id: namespace.id,
+ project_namespace_id: project_namespace.id, visibility_level: 20
+ )
project_statistics_table.create!(project_id: project.id, namespace_id: namespace.id, repository_size: repo_size)
issues_table.create!(project_id: project.id, namespace_id: project.project_namespace_id) if with_issue
diff --git a/spec/lib/gitlab/background_migration/disable_legacy_open_source_license_for_one_member_no_repo_projects_spec.rb b/spec/lib/gitlab/background_migration/disable_legacy_open_source_license_for_one_member_no_repo_projects_spec.rb
index 0dba1d7c8a2..285e5ebbee2 100644
--- a/spec/lib/gitlab/background_migration/disable_legacy_open_source_license_for_one_member_no_repo_projects_spec.rb
+++ b/spec/lib/gitlab/background_migration/disable_legacy_open_source_license_for_one_member_no_repo_projects_spec.rb
@@ -3,8 +3,8 @@
require 'spec_helper'
RSpec.describe Gitlab::BackgroundMigration::DisableLegacyOpenSourceLicenseForOneMemberNoRepoProjects,
- :migration,
- schema: 20220721031446 do
+ :migration,
+ schema: 20220721031446 do
let(:namespaces_table) { table(:namespaces) }
let(:projects_table) { table(:projects) }
let(:project_settings_table) { table(:project_settings) }
@@ -13,18 +13,19 @@ RSpec.describe Gitlab::BackgroundMigration::DisableLegacyOpenSourceLicenseForOne
let(:project_authorizations_table) { table(:project_authorizations) }
subject(:perform_migration) do
- described_class.new(start_id: projects_table.minimum(:id),
- end_id: projects_table.maximum(:id),
- batch_table: :projects,
- batch_column: :id,
- sub_batch_size: 2,
- pause_ms: 0,
- connection: ActiveRecord::Base.connection)
- .perform
+ described_class.new(
+ start_id: projects_table.minimum(:id),
+ end_id: projects_table.maximum(:id),
+ batch_table: :projects,
+ batch_column: :id,
+ sub_batch_size: 2,
+ pause_ms: 0,
+ connection: ActiveRecord::Base.connection
+ ).perform
end
it 'sets `legacy_open_source_license_available` to false only for public projects with 1 member and no repo',
- :aggregate_failures do
+ :aggregate_failures do
project_with_no_repo_one_member = create_legacy_license_public_project('project-with-one-member-no-repo')
project_with_repo_one_member = create_legacy_license_public_project('project-with-repo', repo_size: 1)
project_with_no_repo_two_members = create_legacy_license_public_project('project-with-two-members', members: 2)
@@ -42,13 +43,13 @@ RSpec.describe Gitlab::BackgroundMigration::DisableLegacyOpenSourceLicenseForOne
def create_legacy_license_public_project(path, repo_size: 0, members: 1)
namespace = namespaces_table.create!(name: "namespace-#{path}", path: "namespace-#{path}")
- project_namespace =
- namespaces_table.create!(name: "-project-namespace-#{path}", path: "project-namespace-#{path}", type: 'Project')
- project = projects_table
- .create!(
- name: path, path: path, namespace_id: namespace.id,
- project_namespace_id: project_namespace.id, visibility_level: 20
- )
+ project_namespace = namespaces_table.create!(
+ name: "-project-namespace-#{path}", path: "project-namespace-#{path}", type: 'Project'
+ )
+ project = projects_table.create!(
+ name: path, path: path, namespace_id: namespace.id,
+ project_namespace_id: project_namespace.id, visibility_level: 20
+ )
members.times do |member_id|
user = users_table.create!(email: "user#{member_id}-project-#{project.id}@gitlab.com", projects_limit: 100)
diff --git a/spec/lib/gitlab/background_migration/disable_legacy_open_source_license_for_projects_less_than_five_mb_spec.rb b/spec/lib/gitlab/background_migration/disable_legacy_open_source_license_for_projects_less_than_five_mb_spec.rb
index a153507837c..fedee9c5068 100644
--- a/spec/lib/gitlab/background_migration/disable_legacy_open_source_license_for_projects_less_than_five_mb_spec.rb
+++ b/spec/lib/gitlab/background_migration/disable_legacy_open_source_license_for_projects_less_than_five_mb_spec.rb
@@ -3,23 +3,24 @@
require 'spec_helper'
RSpec.describe Gitlab::BackgroundMigration::DisableLegacyOpenSourceLicenseForProjectsLessThanFiveMb,
- :migration,
- schema: 20221018095434,
- feature_category: :groups_and_projects do
+ :migration,
+ schema: 20221018095434,
+ feature_category: :groups_and_projects do
let(:namespaces_table) { table(:namespaces) }
let(:projects_table) { table(:projects) }
let(:project_settings_table) { table(:project_settings) }
let(:project_statistics_table) { table(:project_statistics) }
subject(:perform_migration) do
- described_class.new(start_id: project_settings_table.minimum(:project_id),
- end_id: project_settings_table.maximum(:project_id),
- batch_table: :project_settings,
- batch_column: :project_id,
- sub_batch_size: 2,
- pause_ms: 0,
- connection: ActiveRecord::Base.connection)
- .perform
+ described_class.new(
+ start_id: project_settings_table.minimum(:project_id),
+ end_id: project_settings_table.maximum(:project_id),
+ batch_table: :project_settings,
+ batch_column: :project_id,
+ sub_batch_size: 2,
+ pause_ms: 0,
+ connection: ActiveRecord::Base.connection
+ ).perform
end
it 'sets `legacy_open_source_license_available` to false only for projects less than 5 MiB', :aggregate_failures do
@@ -45,10 +46,12 @@ RSpec.describe Gitlab::BackgroundMigration::DisableLegacyOpenSourceLicenseForPro
def create_legacy_license_project_setting(repo_size:)
path = "path-for-repo-size-#{repo_size}"
namespace = namespaces_table.create!(name: "namespace-#{path}", path: "namespace-#{path}")
- project_namespace =
- namespaces_table.create!(name: "-project-namespace-#{path}", path: "project-namespace-#{path}", type: 'Project')
- project = projects_table
- .create!(name: path, path: path, namespace_id: namespace.id, project_namespace_id: project_namespace.id)
+ project_namespace = namespaces_table.create!(
+ name: "-project-namespace-#{path}", path: "project-namespace-#{path}", type: 'Project'
+ )
+ project = projects_table.create!(
+ name: path, path: path, namespace_id: namespace.id, project_namespace_id: project_namespace.id
+ )
size_in_bytes = 1.megabyte * repo_size
project_statistics_table.create!(project_id: project.id, namespace_id: namespace.id, repository_size: size_in_bytes)
diff --git a/spec/lib/gitlab/background_migration/disable_legacy_open_source_license_for_projects_less_than_one_mb_spec.rb b/spec/lib/gitlab/background_migration/disable_legacy_open_source_license_for_projects_less_than_one_mb_spec.rb
index 2e6bc2f77ae..cf544c87b31 100644
--- a/spec/lib/gitlab/background_migration/disable_legacy_open_source_license_for_projects_less_than_one_mb_spec.rb
+++ b/spec/lib/gitlab/background_migration/disable_legacy_open_source_license_for_projects_less_than_one_mb_spec.rb
@@ -3,26 +3,27 @@
require 'spec_helper'
RSpec.describe Gitlab::BackgroundMigration::DisableLegacyOpenSourceLicenseForProjectsLessThanOneMb,
- :migration,
- schema: 20220906074449 do
+ :migration,
+ schema: 20220906074449 do
let(:namespaces_table) { table(:namespaces) }
let(:projects_table) { table(:projects) }
let(:project_settings_table) { table(:project_settings) }
let(:project_statistics_table) { table(:project_statistics) }
subject(:perform_migration) do
- described_class.new(start_id: project_settings_table.minimum(:project_id),
- end_id: project_settings_table.maximum(:project_id),
- batch_table: :project_settings,
- batch_column: :project_id,
- sub_batch_size: 2,
- pause_ms: 0,
- connection: ActiveRecord::Base.connection)
- .perform
+ described_class.new(
+ start_id: project_settings_table.minimum(:project_id),
+ end_id: project_settings_table.maximum(:project_id),
+ batch_table: :project_settings,
+ batch_column: :project_id,
+ sub_batch_size: 2,
+ pause_ms: 0,
+ connection: ActiveRecord::Base.connection
+ ).perform
end
it 'sets `legacy_open_source_license_available` to false only for projects less than 1 MiB',
- :aggregate_failures do
+ :aggregate_failures do
project_setting_1_mb = create_legacy_license_project_setting(repo_size: 1)
project_setting_2_mb = create_legacy_license_project_setting(repo_size: 2)
project_setting_quarter_mb = create_legacy_license_project_setting(repo_size: 0.25)
@@ -43,10 +44,12 @@ RSpec.describe Gitlab::BackgroundMigration::DisableLegacyOpenSourceLicenseForPro
def create_legacy_license_project_setting(repo_size:)
path = "path-for-repo-size-#{repo_size}"
namespace = namespaces_table.create!(name: "namespace-#{path}", path: "namespace-#{path}")
- project_namespace =
- namespaces_table.create!(name: "-project-namespace-#{path}", path: "project-namespace-#{path}", type: 'Project')
- project = projects_table
- .create!(name: path, path: path, namespace_id: namespace.id, project_namespace_id: project_namespace.id)
+ project_namespace = namespaces_table.create!(
+ name: "-project-namespace-#{path}", path: "project-namespace-#{path}", type: 'Project'
+ )
+ project = projects_table.create!(
+ name: path, path: path, namespace_id: namespace.id, project_namespace_id: project_namespace.id
+ )
size_in_bytes = 1.megabyte * repo_size
project_statistics_table.create!(project_id: project.id, namespace_id: namespace.id, repository_size: size_in_bytes)
diff --git a/spec/lib/gitlab/background_migration/expire_o_auth_tokens_spec.rb b/spec/lib/gitlab/background_migration/expire_o_auth_tokens_spec.rb
index cffcda0a2ca..ba3aab03f2a 100644
--- a/spec/lib/gitlab/background_migration/expire_o_auth_tokens_spec.rb
+++ b/spec/lib/gitlab/background_migration/expire_o_auth_tokens_spec.rb
@@ -9,14 +9,15 @@ RSpec.describe Gitlab::BackgroundMigration::ExpireOAuthTokens, :migration, schem
let(:table_name) { 'oauth_access_tokens' }
subject(:perform_migration) do
- described_class.new(start_id: 1,
- end_id: 30,
- batch_table: :oauth_access_tokens,
- batch_column: :id,
- sub_batch_size: 2,
- pause_ms: 0,
- connection: ActiveRecord::Base.connection)
- .perform
+ described_class.new(
+ start_id: 1,
+ end_id: 30,
+ batch_table: :oauth_access_tokens,
+ batch_column: :id,
+ sub_batch_size: 2,
+ pause_ms: 0,
+ connection: ActiveRecord::Base.connection
+ ).perform
end
before do
diff --git a/spec/lib/gitlab/background_migration/fix_incoherent_packages_size_on_project_statistics_spec.rb b/spec/lib/gitlab/background_migration/fix_incoherent_packages_size_on_project_statistics_spec.rb
index f71b54a7eb4..2a53d39b6b1 100644
--- a/spec/lib/gitlab/background_migration/fix_incoherent_packages_size_on_project_statistics_spec.rb
+++ b/spec/lib/gitlab/background_migration/fix_incoherent_packages_size_on_project_statistics_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
# rubocop: disable RSpec/MultipleMemoizedHelpers
RSpec.describe Gitlab::BackgroundMigration::FixIncoherentPackagesSizeOnProjectStatistics,
- feature_category: :package_registry do
+ feature_category: :package_registry do
let(:project_statistics_table) { table(:project_statistics) }
let(:packages_table) { table(:packages_packages) }
let(:package_files_table) { table(:packages_package_files) }
@@ -197,8 +197,8 @@ RSpec.describe Gitlab::BackgroundMigration::FixIncoherentPackagesSizeOnProjectSt
context 'with incoherent packages_size' do
it_behaves_like 'enqueuing a buffered updates',
- incoherent_non_zero_statistics: 195,
- incoherent_zero_statistics: 200
+ incoherent_non_zero_statistics: 195,
+ incoherent_zero_statistics: 200
context 'with updates waiting on redis' do
before do
@@ -207,8 +207,8 @@ RSpec.describe Gitlab::BackgroundMigration::FixIncoherentPackagesSizeOnProjectSt
end
it_behaves_like 'enqueuing a buffered updates',
- incoherent_non_zero_statistics: 195,
- incoherent_zero_statistics: 200
+ incoherent_non_zero_statistics: 195,
+ incoherent_zero_statistics: 200
end
end
diff --git a/spec/lib/gitlab/background_migration/legacy_upload_mover_spec.rb b/spec/lib/gitlab/background_migration/legacy_upload_mover_spec.rb
index 71e9a568370..f4c5cd79863 100644
--- a/spec/lib/gitlab/background_migration/legacy_upload_mover_spec.rb
+++ b/spec/lib/gitlab/background_migration/legacy_upload_mover_spec.rb
@@ -18,9 +18,14 @@ RSpec.describe Gitlab::BackgroundMigration::LegacyUploadMover, :aggregate_failur
let(:legacy_upload) { create_upload(note, filename) }
def create_remote_upload(model, filename)
- create(:upload, :attachment_upload,
- path: "note/attachment/#{model.id}/#{filename}", secret: nil,
- store: ObjectStorage::Store::REMOTE, model: model)
+ create(
+ :upload,
+ :attachment_upload,
+ path: "note/attachment/#{model.id}/#{filename}",
+ secret: nil,
+ store: ObjectStorage::Store::REMOTE,
+ model: model
+ )
end
def create_upload(model, filename, with_file = true)
@@ -147,14 +152,23 @@ RSpec.describe Gitlab::BackgroundMigration::LegacyUploadMover, :aggregate_failur
end
let(:legacy_upload) do
- create(:upload, :with_file, :attachment_upload,
- path: "uploads/-/system/note/attachment/#{note.id}/#{filename}", model: note)
+ create(
+ :upload,
+ :with_file,
+ :attachment_upload,
+ path: "uploads/-/system/note/attachment/#{note.id}/#{filename}",
+ model: note
+ )
end
context 'when the file does not exist for the upload' do
let(:legacy_upload) do
- create(:upload, :attachment_upload,
- path: "uploads/-/system/note/attachment/#{note.id}/#{filename}", model: note)
+ create(
+ :upload,
+ :attachment_upload,
+ path: "uploads/-/system/note/attachment/#{note.id}/#{filename}",
+ model: note
+ )
end
it_behaves_like 'move error'
@@ -162,8 +176,13 @@ RSpec.describe Gitlab::BackgroundMigration::LegacyUploadMover, :aggregate_failur
context 'when the file does not exist on expected path' do
let(:legacy_upload) do
- create(:upload, :attachment_upload, :with_file,
- path: "uploads/-/system/note/attachment/some_part/#{note.id}/#{filename}", model: note)
+ create(
+ :upload,
+ :attachment_upload,
+ :with_file,
+ path: "uploads/-/system/note/attachment/some_part/#{note.id}/#{filename}",
+ model: note
+ )
end
it_behaves_like 'move error'
@@ -171,8 +190,13 @@ RSpec.describe Gitlab::BackgroundMigration::LegacyUploadMover, :aggregate_failur
context 'when the file path does not include system/note/attachment' do
let(:legacy_upload) do
- create(:upload, :attachment_upload, :with_file,
- path: "uploads/-/system#{note.id}/#{filename}", model: note)
+ create(
+ :upload,
+ :attachment_upload,
+ :with_file,
+ path: "uploads/-/system#{note.id}/#{filename}",
+ model: note
+ )
end
it_behaves_like 'move error'
@@ -188,8 +212,14 @@ RSpec.describe Gitlab::BackgroundMigration::LegacyUploadMover, :aggregate_failur
context 'when upload has mount_point nil' do
let(:legacy_upload) do
- create(:upload, :with_file, :attachment_upload,
- path: "uploads/-/system/note/attachment/#{note.id}/#{filename}", model: note, mount_point: nil)
+ create(
+ :upload,
+ :with_file,
+ :attachment_upload,
+ path: "uploads/-/system/note/attachment/#{note.id}/#{filename}",
+ model: note,
+ mount_point: nil
+ )
end
it_behaves_like 'migrates the file correctly', false
diff --git a/spec/lib/gitlab/background_migration/remove_backfilled_job_artifacts_expire_at_spec.rb b/spec/lib/gitlab/background_migration/remove_backfilled_job_artifacts_expire_at_spec.rb
index af8b5240e40..4c989ba9cef 100644
--- a/spec/lib/gitlab/background_migration/remove_backfilled_job_artifacts_expire_at_spec.rb
+++ b/spec/lib/gitlab/background_migration/remove_backfilled_job_artifacts_expire_at_spec.rb
@@ -7,7 +7,7 @@ RSpec.describe Gitlab::BackgroundMigration::RemoveBackfilledJobArtifactsExpireAt
describe '#perform' do
let(:job_artifact) { table(:ci_job_artifacts, database: :ci) }
- let(:jobs) { table(:ci_builds, database: :ci) { |model| model.primary_key = :id } }
+ let(:jobs) { table(:p_ci_builds, database: :ci) { |model| model.primary_key = :id } }
let(:test_worker) do
described_class.new(
diff --git a/spec/lib/gitlab/batch_worker_context_spec.rb b/spec/lib/gitlab/batch_worker_context_spec.rb
index 31641f7449e..a0a5bf0cba1 100644
--- a/spec/lib/gitlab/batch_worker_context_spec.rb
+++ b/spec/lib/gitlab/batch_worker_context_spec.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe Gitlab::BatchWorkerContext do
subject(:batch_context) do
described_class.new(
- %w(hello world),
+ %w[hello world],
arguments_proc: -> (word) { word },
context_proc: -> (word) { { user: build_stubbed(:user, username: word) } }
)
@@ -13,13 +13,13 @@ RSpec.describe Gitlab::BatchWorkerContext do
describe "#arguments" do
it "returns all the expected arguments in arrays" do
- expect(batch_context.arguments).to eq([%w(hello), %w(world)])
+ expect(batch_context.arguments).to eq([%w[hello], %w[world]])
end
end
describe "#context_for" do
it "returns the correct application context for the arguments" do
- context = batch_context.context_for(%w(world))
+ context = batch_context.context_for(%w[world])
expect(context).to be_a(Gitlab::ApplicationContext)
expect(context.to_lazy_hash[:user].call).to eq("world")
diff --git a/spec/lib/gitlab/bitbucket_import/importer_spec.rb b/spec/lib/gitlab/bitbucket_import/importer_spec.rb
index 517d557d665..d468483661a 100644
--- a/spec/lib/gitlab/bitbucket_import/importer_spec.rb
+++ b/spec/lib/gitlab/bitbucket_import/importer_spec.rb
@@ -220,7 +220,7 @@ RSpec.describe Gitlab::BitbucketImport::Importer, :clean_gitlab_redis_cache, fea
subject.execute
expect(subject.errors.count).to eq(1)
- expect(subject.errors.first.keys).to match_array(%i(type iid errors))
+ expect(subject.errors.first.keys).to match_array(%i[type iid errors])
end
end
diff --git a/spec/lib/gitlab/bitbucket_import/importers/issue_importer_spec.rb b/spec/lib/gitlab/bitbucket_import/importers/issue_importer_spec.rb
index 8f79390d2d9..8732c787657 100644
--- a/spec/lib/gitlab/bitbucket_import/importers/issue_importer_spec.rb
+++ b/spec/lib/gitlab/bitbucket_import/importers/issue_importer_spec.rb
@@ -99,5 +99,13 @@ RSpec.describe Gitlab::BitbucketImport::Importers::IssueImporter, :clean_gitlab_
importer.execute
end
+
+ it 'increments the issue counter' do
+ expect_next_instance_of(Gitlab::Import::Metrics) do |metrics|
+ expect(metrics).to receive_message_chain(:issues_counter, :increment)
+ end
+
+ importer.execute
+ end
end
end
diff --git a/spec/lib/gitlab/bitbucket_import/importers/issues_importer_spec.rb b/spec/lib/gitlab/bitbucket_import/importers/issues_importer_spec.rb
index a361a9343dd..af5a929683e 100644
--- a/spec/lib/gitlab/bitbucket_import/importers/issues_importer_spec.rb
+++ b/spec/lib/gitlab/bitbucket_import/importers/issues_importer_spec.rb
@@ -12,22 +12,37 @@ RSpec.describe Gitlab::BitbucketImport::Importers::IssuesImporter, feature_categ
)
end
+ let(:client) { Bitbucket::Client.new(project.import_data.credentials) }
+
+ before do
+ allow(Bitbucket::Client).to receive(:new).and_return(client)
+ allow(client).to receive(:repo).and_return(Bitbucket::Representation::Repo.new({ 'has_issues' => true }))
+ allow(client).to receive(:last_issue).and_return(Bitbucket::Representation::Issue.new({ 'id' => 2 }))
+ allow(client).to receive(:issues).and_return(
+ [
+ Bitbucket::Representation::Issue.new({ 'id' => 1 }),
+ Bitbucket::Representation::Issue.new({ 'id' => 2 })
+ ],
+ []
+ )
+ end
+
subject(:importer) { described_class.new(project) }
describe '#execute', :clean_gitlab_redis_cache do
- before do
- allow_next_instance_of(Bitbucket::Client) do |client|
- allow(client).to receive(:issues).and_return(
- [
- Bitbucket::Representation::Issue.new({ 'id' => 1 }),
- Bitbucket::Representation::Issue.new({ 'id' => 2 })
- ],
- []
- )
+ context 'when the repo does not have issue tracking enabled' do
+ before do
+ allow(client).to receive(:repo).and_return(Bitbucket::Representation::Repo.new({ 'has_issues' => false }))
+ end
+
+ it 'does not import issues' do
+ expect(Gitlab::BitbucketImport::ImportIssueWorker).not_to receive(:perform_in)
+
+ importer.execute
end
end
- it 'imports each issue in parallel', :aggregate_failures do
+ it 'imports each issue in parallel' do
expect(Gitlab::BitbucketImport::ImportIssueWorker).to receive(:perform_in).twice
waiter = importer.execute
@@ -38,11 +53,15 @@ RSpec.describe Gitlab::BitbucketImport::Importers::IssuesImporter, feature_categ
.to match_array(%w[1 2])
end
+ it 'allocates internal ids' do
+ expect(Issue).to receive(:track_namespace_iid!).with(project.project_namespace, 2)
+
+ importer.execute
+ end
+
context 'when the client raises an error' do
before do
- allow_next_instance_of(Bitbucket::Client) do |client|
- allow(client).to receive(:issues).and_raise(StandardError)
- end
+ allow(client).to receive(:issues).and_raise(StandardError)
end
it 'tracks the failure and does not fail' do
@@ -57,7 +76,7 @@ RSpec.describe Gitlab::BitbucketImport::Importers::IssuesImporter, feature_categ
Gitlab::Cache::Import::Caching.set_add(importer.already_enqueued_cache_key, 1)
end
- it 'does not schedule job for enqueued issues', :aggregate_failures do
+ it 'does not schedule job for enqueued issues' do
expect(Gitlab::BitbucketImport::ImportIssueWorker).to receive(:perform_in).once
waiter = importer.execute
diff --git a/spec/lib/gitlab/bitbucket_import/importers/issues_notes_importer_spec.rb b/spec/lib/gitlab/bitbucket_import/importers/issues_notes_importer_spec.rb
index 043cd7f17b9..a04543b0511 100644
--- a/spec/lib/gitlab/bitbucket_import/importers/issues_notes_importer_spec.rb
+++ b/spec/lib/gitlab/bitbucket_import/importers/issues_notes_importer_spec.rb
@@ -4,15 +4,13 @@ require 'spec_helper'
RSpec.describe Gitlab::BitbucketImport::Importers::IssuesNotesImporter, feature_category: :importers do
let_it_be(:project) { create(:project, :import_started) }
- # let_it_be(:merge_request_1) { create(:merge_request, source_project: project) }
- # let_it_be(:merge_request_2) { create(:merge_request, source_project: project, source_branch: 'other-branch') }
let_it_be(:issue_1) { create(:issue, project: project) }
let_it_be(:issue_2) { create(:issue, project: project) }
subject(:importer) { described_class.new(project) }
describe '#execute', :clean_gitlab_redis_cache do
- it 'imports the notes from each issue in parallel', :aggregate_failures do
+ it 'imports the notes from each issue in parallel' do
expect(Gitlab::BitbucketImport::ImportIssueNotesWorker).to receive(:perform_in).twice
waiter = importer.execute
@@ -40,7 +38,7 @@ RSpec.describe Gitlab::BitbucketImport::Importers::IssuesNotesImporter, feature_
Gitlab::Cache::Import::Caching.set_add(importer.already_enqueued_cache_key, 2)
end
- it 'does not schedule job for enqueued issues', :aggregate_failures do
+ it 'does not schedule job for enqueued issues' do
expect(Gitlab::BitbucketImport::ImportIssueNotesWorker).to receive(:perform_in).once
waiter = importer.execute
diff --git a/spec/lib/gitlab/bitbucket_import/importers/pull_request_importer_spec.rb b/spec/lib/gitlab/bitbucket_import/importers/pull_request_importer_spec.rb
index 2eca6bb47d6..1f36a353724 100644
--- a/spec/lib/gitlab/bitbucket_import/importers/pull_request_importer_spec.rb
+++ b/spec/lib/gitlab/bitbucket_import/importers/pull_request_importer_spec.rb
@@ -162,5 +162,13 @@ RSpec.describe Gitlab::BitbucketImport::Importers::PullRequestImporter, :clean_g
importer.execute
end
+
+ it 'increments the merge requests counter' do
+ expect_next_instance_of(Gitlab::Import::Metrics) do |metrics|
+ expect(metrics).to receive_message_chain(:merge_requests_counter, :increment)
+ end
+
+ importer.execute
+ end
end
end
diff --git a/spec/lib/gitlab/bitbucket_import/importers/pull_request_notes_importer_spec.rb b/spec/lib/gitlab/bitbucket_import/importers/pull_request_notes_importer_spec.rb
index 4a30f225d66..332f6e5bd03 100644
--- a/spec/lib/gitlab/bitbucket_import/importers/pull_request_notes_importer_spec.rb
+++ b/spec/lib/gitlab/bitbucket_import/importers/pull_request_notes_importer_spec.rb
@@ -2,9 +2,9 @@
require 'spec_helper'
-RSpec.describe Gitlab::BitbucketImport::Importers::PullRequestNotesImporter, feature_category: :importers do
+RSpec.describe Gitlab::BitbucketImport::Importers::PullRequestNotesImporter, :clean_gitlab_redis_cache, feature_category: :importers do
let_it_be(:project) do
- create(:project, :import_started,
+ create(:project, :repository, :import_started,
import_data_attributes: {
credentials: { 'base_uri' => 'http://bitbucket.org/', 'user' => 'bitbucket', 'password' => 'password' }
}
@@ -12,28 +12,216 @@ RSpec.describe Gitlab::BitbucketImport::Importers::PullRequestNotesImporter, fea
end
let_it_be(:merge_request) { create(:merge_request, source_project: project) }
-
+ let_it_be(:merge_request_diff) { create(:merge_request_diff, :external, merge_request: merge_request) }
+ let_it_be(:bitbucket_user) { create(:user) }
+ let_it_be(:identity) { create(:identity, user: bitbucket_user, extern_uid: 'bitbucket_user', provider: :bitbucket) }
let(:hash) { { iid: merge_request.iid } }
- let(:importer_helper) { Gitlab::BitbucketImport::Importer.new(project) }
+ let(:client) { Bitbucket::Client.new({}) }
+ let(:ref_converter) { Gitlab::BitbucketImport::RefConverter.new(project) }
+ let(:user_finder) { Gitlab::BitbucketImport::UserFinder.new(project) }
+ let(:note_body) { 'body' }
+ let(:comments) { [Bitbucket::Representation::PullRequestComment.new(note_hash)] }
+ let(:created_at) { Date.today - 2.days }
+ let(:updated_at) { Date.today }
+ let(:note_hash) do
+ {
+ 'id' => 12,
+ 'user' => { 'nickname' => 'bitbucket_user' },
+ 'content' => { 'raw' => note_body },
+ 'created_on' => created_at,
+ 'updated_on' => updated_at
+ }
+ end
subject(:importer) { described_class.new(project, hash) }
before do
- allow(Gitlab::BitbucketImport::Importer).to receive(:new).and_return(importer_helper)
+ allow(Bitbucket::Client).to receive(:new).and_return(client)
+ allow(Gitlab::BitbucketImport::RefConverter).to receive(:new).and_return(ref_converter)
+ allow(Gitlab::BitbucketImport::UserFinder).to receive(:new).and_return(user_finder)
+ allow(client).to receive(:pull_request_comments).and_return(comments)
end
describe '#execute' do
- it 'calls Importer.import_pull_request_comments' do
- expect(importer_helper).to receive(:import_pull_request_comments).once
+ context 'for standalone pr comments' do
+ it 'calls RefConverter' do
+ expect(ref_converter).to receive(:convert_note).once.and_call_original
+
+ importer.execute
+ end
+
+ it 'creates a note with the correct attributes' do
+ expect { importer.execute }.to change { merge_request.notes.count }.from(0).to(1)
+
+ note = merge_request.notes.first
+
+ expect(note.note).to eq(note_body)
+ expect(note.author).to eq(bitbucket_user)
+ expect(note.created_at).to eq(created_at)
+ expect(note.updated_at).to eq(updated_at)
+ end
+
+ context 'when the author does not have a bitbucket identity' do
+ before do
+ identity.update!(provider: :github)
+ end
+
+ it 'sets the author to the project creator and adds the author to the note' do
+ importer.execute
+
+ note = merge_request.notes.first
+
+ expect(note.author).to eq(project.creator)
+ expect(note.note).to eq("*Created by: bitbucket_user*\n\nbody")
+ end
+ end
+
+ context 'when the note is deleted' do
+ let(:note_hash) do
+ {
+ 'id' => 12,
+ 'user' => { 'nickname' => 'bitbucket_user' },
+ 'content' => { 'raw' => note_body },
+ 'deleted' => true,
+ 'created_on' => created_at,
+ 'updated_on' => updated_at
+ }
+ end
+
+ it 'does not create a note' do
+ expect { importer.execute }.not_to change { merge_request.notes.count }
+ end
+ end
+ end
+
+ context 'for threaded inline comments' do
+ let(:path) { project.repository.commit.raw_diffs.first.new_path }
+ let(:reply_body) { 'Some reply' }
+ let(:comments) do
+ [
+ Bitbucket::Representation::PullRequestComment.new(pr_comment_1),
+ Bitbucket::Representation::PullRequestComment.new(pr_comment_2)
+ ]
+ end
- importer.execute
+ let(:pr_comment_1) do
+ {
+ 'id' => 14,
+ 'inline' => {
+ 'path' => path,
+ 'from' => nil,
+ 'to' => 1
+ },
+ 'parent' => { 'id' => 13 },
+ 'user' => { 'nickname' => 'bitbucket_user' },
+ 'content' => { 'raw' => reply_body },
+ 'created_on' => created_at,
+ 'updated_on' => updated_at
+ }
+ end
+
+ let(:pr_comment_2) do
+ {
+ 'id' => 13,
+ 'inline' => {
+ 'path' => path,
+ 'from' => nil,
+ 'to' => 1
+ },
+ 'user' => { 'nickname' => 'non_existent_user' },
+ 'content' => { 'raw' => note_body },
+ 'created_on' => created_at,
+ 'updated_on' => updated_at
+ }
+ end
+
+ it 'creates notes in the correct position with the right attributes' do
+ expect { importer.execute }.to change { merge_request.notes.count }.from(0).to(2)
+
+ expect(merge_request.notes.map(&:discussion_id).uniq.count).to eq(1)
+
+ notes = merge_request.notes.order(:id).to_a
+
+ start_note = notes.first
+ expect(start_note).to be_a(DiffNote)
+ expect(start_note.note).to eq("*Created by: non_existent_user*\n\n#{note_body}")
+ expect(start_note.author).to eq(project.creator)
+
+ reply_note = notes.last
+ expect(reply_note).to be_a(DiffNote)
+ expect(reply_note.note).to eq(reply_body)
+ expect(reply_note.author).to eq(bitbucket_user)
+ end
+
+ context 'when the comments are not part of the diff' do
+ let(:pr_comment_1) do
+ {
+ 'id' => 14,
+ 'inline' => {
+ 'path' => path,
+ 'from' => nil,
+ 'to' => nil
+ },
+ 'parent' => { 'id' => 13 },
+ 'user' => { 'nickname' => 'bitbucket_user' },
+ 'content' => { 'raw' => reply_body },
+ 'created_on' => created_at,
+ 'updated_on' => updated_at
+ }
+ end
+
+ let(:pr_comment_2) do
+ {
+ 'id' => 13,
+ 'inline' => {
+ 'path' => path,
+ 'from' => nil,
+ 'to' => nil
+ },
+ 'user' => { 'nickname' => 'bitbucket_user' },
+ 'content' => { 'raw' => note_body },
+ 'created_on' => created_at,
+ 'updated_on' => updated_at
+ }
+ end
+
+ it 'creates them as normal notes' do
+ expect { importer.execute }.to change { merge_request.notes.count }.from(0).to(2)
+
+ notes = merge_request.notes.order(:id).to_a
+
+ first_note = notes.first
+ expect(first_note).not_to be_a(DiffNote)
+ expect(first_note.note).to eq("*Comment on*\n\n#{note_body}")
+ expect(first_note.author).to eq(bitbucket_user)
+
+ second_note = notes.last
+ expect(second_note).not_to be_a(DiffNote)
+ expect(second_note.note).to eq("*Comment on*\n\n#{reply_body}")
+ expect(second_note.author).to eq(bitbucket_user)
+ end
+ end
+
+ context 'when an error is raised for one note' do
+ before do
+ allow(user_finder).to receive(:gitlab_user_id).and_call_original
+ allow(user_finder).to receive(:gitlab_user_id).with(project, 'bitbucket_user').and_raise(StandardError)
+ end
+
+ it 'tracks the error and continues to import other notes' do
+ expect(Gitlab::ErrorTracking).to receive(:log_exception)
+ .with(anything, hash_including(comment_id: 14)).and_call_original
+
+ expect { importer.execute }.to change { merge_request.notes.count }.from(0).to(1)
+ end
+ end
end
context 'when the merge request does not exist' do
let(:hash) { { iid: 'nonexistent' } }
- it 'does not call Importer.import_pull_request_comments' do
- expect(importer_helper).not_to receive(:import_pull_request_comments)
+ it 'does not call #import_pull_request_comments' do
+ expect(importer).not_to receive(:import_pull_request_comments)
importer.execute
end
@@ -46,8 +234,8 @@ RSpec.describe Gitlab::BitbucketImport::Importers::PullRequestNotesImporter, fea
merge_request.update!(source_project: another_project, target_project: another_project)
end
- it 'does not call Importer.import_pull_request_comments' do
- expect(importer_helper).not_to receive(:import_pull_request_comments)
+ it 'does not call #import_pull_request_comments' do
+ expect(importer).not_to receive(:import_pull_request_comments)
importer.execute
end
@@ -55,7 +243,7 @@ RSpec.describe Gitlab::BitbucketImport::Importers::PullRequestNotesImporter, fea
context 'when an error is raised' do
before do
- allow(importer_helper).to receive(:import_pull_request_comments).and_raise(StandardError)
+ allow(importer).to receive(:import_pull_request_comments).and_raise(StandardError)
end
it 'tracks the failure and does not fail' do
diff --git a/spec/lib/gitlab/bitbucket_import/importers/pull_requests_importer_spec.rb b/spec/lib/gitlab/bitbucket_import/importers/pull_requests_importer_spec.rb
index 46bf099de0c..eba7ec92aba 100644
--- a/spec/lib/gitlab/bitbucket_import/importers/pull_requests_importer_spec.rb
+++ b/spec/lib/gitlab/bitbucket_import/importers/pull_requests_importer_spec.rb
@@ -28,7 +28,7 @@ RSpec.describe Gitlab::BitbucketImport::Importers::PullRequestsImporter, feature
end
end
- it 'imports each pull request in parallel', :aggregate_failures do
+ it 'imports each pull request in parallel' do
expect(Gitlab::BitbucketImport::ImportPullRequestWorker).to receive(:perform_in).exactly(3).times
waiter = importer.execute
@@ -58,7 +58,7 @@ RSpec.describe Gitlab::BitbucketImport::Importers::PullRequestsImporter, feature
Gitlab::Cache::Import::Caching.set_add(importer.already_enqueued_cache_key, 1)
end
- it 'does not schedule job for enqueued pull requests', :aggregate_failures do
+ it 'does not schedule job for enqueued pull requests' do
expect(Gitlab::BitbucketImport::ImportPullRequestWorker).to receive(:perform_in).twice
waiter = importer.execute
diff --git a/spec/lib/gitlab/bitbucket_import/importers/pull_requests_notes_importer_spec.rb b/spec/lib/gitlab/bitbucket_import/importers/pull_requests_notes_importer_spec.rb
index c44fc259c3b..78a08accf82 100644
--- a/spec/lib/gitlab/bitbucket_import/importers/pull_requests_notes_importer_spec.rb
+++ b/spec/lib/gitlab/bitbucket_import/importers/pull_requests_notes_importer_spec.rb
@@ -10,7 +10,7 @@ RSpec.describe Gitlab::BitbucketImport::Importers::PullRequestsNotesImporter, fe
subject(:importer) { described_class.new(project) }
describe '#execute', :clean_gitlab_redis_cache do
- it 'imports the notes from each merge request in parallel', :aggregate_failures do
+ it 'imports the notes from each merge request in parallel' do
expect(Gitlab::BitbucketImport::ImportPullRequestNotesWorker).to receive(:perform_in).twice
waiter = importer.execute
@@ -38,7 +38,7 @@ RSpec.describe Gitlab::BitbucketImport::Importers::PullRequestsNotesImporter, fe
Gitlab::Cache::Import::Caching.set_add(importer.already_enqueued_cache_key, 2)
end
- it 'does not schedule job for enqueued merge requests', :aggregate_failures do
+ it 'does not schedule job for enqueued merge requests' do
expect(Gitlab::BitbucketImport::ImportPullRequestNotesWorker).to receive(:perform_in).once
waiter = importer.execute
diff --git a/spec/lib/gitlab/bitbucket_import/importers/repository_importer_spec.rb b/spec/lib/gitlab/bitbucket_import/importers/repository_importer_spec.rb
index 1caf0b884c2..9e458780c78 100644
--- a/spec/lib/gitlab/bitbucket_import/importers/repository_importer_spec.rb
+++ b/spec/lib/gitlab/bitbucket_import/importers/repository_importer_spec.rb
@@ -7,6 +7,14 @@ RSpec.describe Gitlab::BitbucketImport::Importers::RepositoryImporter, feature_c
subject(:importer) { described_class.new(project) }
+ before do
+ allow_next_instance_of(Bitbucket::Client) do |client|
+ allow(client).to receive(:repo).and_return(Bitbucket::Representation::Repo.new(
+ { 'mainbranch' => { 'name' => 'develop' } }
+ ))
+ end
+ end
+
describe '#execute' do
context 'when repository is empty' do
it 'imports the repository' do
@@ -17,6 +25,15 @@ RSpec.describe Gitlab::BitbucketImport::Importers::RepositoryImporter, feature_c
importer.execute
end
+
+ it 'sets the default branch' do
+ allow(project.repository).to receive(:import_repository)
+ allow(project.repository).to receive(:fetch_as_mirror)
+
+ expect(project).to receive(:change_head).with('develop')
+
+ importer.execute
+ end
end
context 'when repository is not empty' do
diff --git a/spec/lib/gitlab/bullet/exclusions_spec.rb b/spec/lib/gitlab/bullet/exclusions_spec.rb
index ccedfee28c7..2cb824674dc 100644
--- a/spec/lib/gitlab/bullet/exclusions_spec.rb
+++ b/spec/lib/gitlab/bullet/exclusions_spec.rb
@@ -3,7 +3,7 @@
require 'fast_spec_helper'
require 'tempfile'
-RSpec.describe Gitlab::Bullet::Exclusions, feature_category: :application_performance do
+RSpec.describe Gitlab::Bullet::Exclusions, feature_category: :cloud_connector do
let(:config_file) do
file = Tempfile.new('bullet.yml')
File.basename(file)
diff --git a/spec/lib/gitlab/cache_spec.rb b/spec/lib/gitlab/cache_spec.rb
index 67c70a77880..92a5ea7bdfb 100644
--- a/spec/lib/gitlab/cache_spec.rb
+++ b/spec/lib/gitlab/cache_spec.rb
@@ -28,7 +28,7 @@ RSpec.describe Gitlab::Cache, :request_store do
end
describe '.delete' do
- let(:key) { %w{a cache key} }
+ let(:key) { %w[a cache key] }
subject(:delete) { described_class.delete(key) }
diff --git a/spec/lib/gitlab/ci/ansi2html_spec.rb b/spec/lib/gitlab/ci/ansi2html_spec.rb
index 30359a7170f..2990599f840 100644
--- a/spec/lib/gitlab/ci/ansi2html_spec.rb
+++ b/spec/lib/gitlab/ci/ansi2html_spec.rb
@@ -227,7 +227,7 @@ RSpec.describe Gitlab::Ci::Ansi2html do
text = "#{section_start}Some text#{section_end}"
class_name_start = section_start.gsub("\033[0K", '').gsub('<', '&lt;')
class_name_end = section_end.gsub("\033[0K", '').gsub('<', '&lt;')
- html = %{<span>#{class_name_start}Some text#{class_name_end}</span>}
+ html = %(<span>#{class_name_start}Some text#{class_name_end}</span>)
expect(convert_html(text)).to eq(html)
end
@@ -238,9 +238,9 @@ RSpec.describe Gitlab::Ci::Ansi2html do
it 'prints light red' do
text = "#{section_start}\e[91mHello\e[0m\nLine 1\nLine 2\nLine 3\n#{section_end}"
- header = %{<span class="term-fg-l-red section section-header js-s-#{class_name(section_name)}">Hello</span>}
- line_break = %{<span class="section section-header js-s-#{class_name(section_name)}"><br/></span>}
- output_line = %{<span class="section line js-s-#{class_name(section_name)}">Line 1<br/>Line 2<br/>Line 3<br/></span>}
+ header = %(<span class="term-fg-l-red section section-header js-s-#{class_name(section_name)}">Hello</span>)
+ line_break = %(<span class="section section-header js-s-#{class_name(section_name)}"><br/></span>)
+ output_line = %(<span class="section line js-s-#{class_name(section_name)}">Line 1<br/>Line 2<br/>Line 3<br/></span>)
html = "#{section_start_html}#{header}#{line_break}#{output_line}#{section_end_html}"
expect(convert_html(text)).to eq(html)
diff --git a/spec/lib/gitlab/ci/ansi2json/line_spec.rb b/spec/lib/gitlab/ci/ansi2json/line_spec.rb
index b8563bb1d1c..475a54b275d 100644
--- a/spec/lib/gitlab/ci/ansi2json/line_spec.rb
+++ b/spec/lib/gitlab/ci/ansi2json/line_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::Ci::Ansi2json::Line do
+RSpec.describe Gitlab::Ci::Ansi2json::Line, feature_category: :continuous_integration do
let(:offset) { 0 }
let(:style) { Gitlab::Ci::Ansi2json::Style.new }
@@ -75,6 +75,14 @@ RSpec.describe Gitlab::Ci::Ansi2json::Line do
end
end
+ describe '#set_as_section_footer' do
+ it 'change the section_footer to true' do
+ expect { subject.set_as_section_footer }
+ .to change { subject.section_footer }
+ .to be_truthy
+ end
+ end
+
describe '#set_section_duration' do
using RSpec::Parameterized::TableSyntax
@@ -178,6 +186,23 @@ RSpec.describe Gitlab::Ci::Ansi2json::Line do
expect(subject.to_h).to eq(result)
end
end
+
+ context 'when section footer is set' do
+ before do
+ subject.set_as_section_footer
+ end
+
+ it 'serializes the attributes set' do
+ result = {
+ offset: 0,
+ content: [{ text: 'some data', style: 'term-bold' }],
+ section: 'section_2',
+ section_footer: true
+ }
+
+ expect(subject.to_h).to eq(result)
+ end
+ end
end
context 'when there are no sections' do
diff --git a/spec/lib/gitlab/ci/ansi2json/state_spec.rb b/spec/lib/gitlab/ci/ansi2json/state_spec.rb
index 8dd4092f3d8..07e6579829a 100644
--- a/spec/lib/gitlab/ci/ansi2json/state_spec.rb
+++ b/spec/lib/gitlab/ci/ansi2json/state_spec.rb
@@ -8,7 +8,7 @@ RSpec.describe Gitlab::Ci::Ansi2json::State, feature_category: :continuous_integ
state.offset = 1
state.new_line!(style: { fg: 'some-fg', bg: 'some-bg', mask: 1234 })
state.set_last_line_offset
- state.open_section('hello', 111, {})
+ state.open_section('hello', 100, {})
end
end
@@ -24,7 +24,7 @@ RSpec.describe Gitlab::Ci::Ansi2json::State, feature_category: :continuous_integ
fg: 'some-fg',
mask: 1234
})
- expect(new_state.open_sections).to eq({ 'hello' => 111 })
+ expect(new_state.open_sections).to eq({ 'hello' => 100 })
end
it 'ignores unsigned prior state', :aggregate_failures do
@@ -44,6 +44,23 @@ RSpec.describe Gitlab::Ci::Ansi2json::State, feature_category: :continuous_integ
expect(new_state.open_sections).to eq({})
end
+ it 'opens and closes a section', :aggregate_failures do
+ new_state = described_class.new('', 1000)
+
+ new_state.new_line!(style: {})
+ new_state.open_section('hello', 100, {})
+
+ expect(new_state.current_line.section_header).to eq(true)
+ expect(new_state.current_line.section_footer).to eq(false)
+
+ new_state.new_line!(style: {})
+ new_state.close_section('hello', 101)
+
+ expect(new_state.current_line.section_header).to eq(false)
+ expect(new_state.current_line.section_duration).to eq('00:01')
+ expect(new_state.current_line.section_footer).to eq(true)
+ end
+
it 'ignores bad input', :aggregate_failures do
expect(::Gitlab::AppLogger).to(
receive(:warn).with(
diff --git a/spec/lib/gitlab/ci/ansi2json_spec.rb b/spec/lib/gitlab/ci/ansi2json_spec.rb
index 98fca40e8ea..23be3209171 100644
--- a/spec/lib/gitlab/ci/ansi2json_spec.rb
+++ b/spec/lib/gitlab/ci/ansi2json_spec.rb
@@ -145,6 +145,7 @@ RSpec.describe Gitlab::Ci::Ansi2json, feature_category: :continuous_integration
offset: 63,
content: [],
section_duration: '01:03',
+ section_footer: true,
section: 'prepare-script'
}
])
@@ -163,7 +164,8 @@ RSpec.describe Gitlab::Ci::Ansi2json, feature_category: :continuous_integration
offset: 56,
content: [],
section: 'prepare-script',
- section_duration: '01:03'
+ section_duration: '01:03',
+ section_footer: true
}
])
end
@@ -181,7 +183,8 @@ RSpec.describe Gitlab::Ci::Ansi2json, feature_category: :continuous_integration
offset: 49,
content: [],
section: 'prepare-script',
- section_duration: '01:03'
+ section_duration: '01:03',
+ section_footer: true
},
{
offset: 91,
@@ -262,7 +265,8 @@ RSpec.describe Gitlab::Ci::Ansi2json, feature_category: :continuous_integration
offset: 75,
content: [],
section: 'prepare-script',
- section_duration: '01:03'
+ section_duration: '01:03',
+ section_footer: true
}
])
end
@@ -300,7 +304,8 @@ RSpec.describe Gitlab::Ci::Ansi2json, feature_category: :continuous_integration
offset: 106,
content: [],
section: 'prepare-script-nested',
- section_duration: '00:02'
+ section_duration: '00:02',
+ section_footer: true
},
{
offset: 155,
@@ -311,7 +316,8 @@ RSpec.describe Gitlab::Ci::Ansi2json, feature_category: :continuous_integration
offset: 158,
content: [],
section: 'prepare-script',
- section_duration: '01:03'
+ section_duration: '01:03',
+ section_footer: true
},
{
offset: 200,
@@ -345,13 +351,15 @@ RSpec.describe Gitlab::Ci::Ansi2json, feature_category: :continuous_integration
offset: 115,
content: [],
section: 'prepare-script-nested',
- section_duration: '00:02'
+ section_duration: '00:02',
+ section_footer: true
},
{
offset: 164,
content: [],
section: 'prepare-script',
- section_duration: '01:03'
+ section_duration: '01:03',
+ section_footer: true
}
])
end
@@ -378,7 +386,8 @@ RSpec.describe Gitlab::Ci::Ansi2json, feature_category: :continuous_integration
offset: 83,
content: [],
section: 'prepare-script',
- section_duration: '01:03'
+ section_duration: '01:03',
+ section_footer: true
}
])
end
@@ -554,7 +563,8 @@ RSpec.describe Gitlab::Ci::Ansi2json, feature_category: :continuous_integration
offset: 77,
content: [],
section: 'prepare-script',
- section_duration: '01:03'
+ section_duration: '01:03',
+ section_footer: true
}
]
end
diff --git a/spec/lib/gitlab/ci/badge/pipeline/status_spec.rb b/spec/lib/gitlab/ci/badge/pipeline/status_spec.rb
index 45d0d781090..d6b59d0da64 100644
--- a/spec/lib/gitlab/ci/badge/pipeline/status_spec.rb
+++ b/spec/lib/gitlab/ci/badge/pipeline/status_spec.rb
@@ -117,10 +117,7 @@ RSpec.describe Gitlab::Ci::Badge::Pipeline::Status do
end
def create_pipeline(project, sha, branch)
- pipeline = create(:ci_empty_pipeline,
- project: project,
- sha: sha,
- ref: branch)
+ pipeline = create(:ci_empty_pipeline, project: project, sha: sha, ref: branch)
create(:ci_build, pipeline: pipeline, stage: 'notify')
end
diff --git a/spec/lib/gitlab/ci/build/artifacts/metadata/entry_spec.rb b/spec/lib/gitlab/ci/build/artifacts/metadata/entry_spec.rb
index 7b35c9ba483..6cd9432c6c5 100644
--- a/spec/lib/gitlab/ci/build/artifacts/metadata/entry_spec.rb
+++ b/spec/lib/gitlab/ci/build/artifacts/metadata/entry_spec.rb
@@ -69,9 +69,11 @@ RSpec.describe Gitlab::Ci::Build::Artifacts::Metadata::Entry do
it { is_expected.to all(be_an_instance_of(described_class)) }
it do
- is_expected.to contain_exactly entry('path/dir_1/file_1'),
- entry('path/dir_1/file_b'),
- entry('path/dir_1/subdir/')
+ is_expected.to contain_exactly(
+ entry('path/dir_1/file_1'),
+ entry('path/dir_1/file_b'),
+ entry('path/dir_1/subdir/')
+ )
end
end
@@ -82,8 +84,10 @@ RSpec.describe Gitlab::Ci::Build::Artifacts::Metadata::Entry do
it { is_expected.to all(be_an_instance_of(described_class)) }
it do
- is_expected.to contain_exactly entry('path/dir_1/file_1'),
- entry('path/dir_1/file_b')
+ is_expected.to contain_exactly(
+ entry('path/dir_1/file_1'),
+ entry('path/dir_1/file_b')
+ )
end
end
@@ -103,8 +107,10 @@ RSpec.describe Gitlab::Ci::Build::Artifacts::Metadata::Entry do
it { is_expected.to all(be_an_instance_of(described_class)) }
it do
- is_expected.to contain_exactly entry('path/dir_1/subdir/'),
- entry('path/')
+ is_expected.to contain_exactly(
+ entry('path/dir_1/subdir/'),
+ entry('path/')
+ )
end
end
diff --git a/spec/lib/gitlab/ci/build/context/build_spec.rb b/spec/lib/gitlab/ci/build/context/build_spec.rb
index fae02e140f2..9fdb4ee9393 100644
--- a/spec/lib/gitlab/ci/build/context/build_spec.rb
+++ b/spec/lib/gitlab/ci/build/context/build_spec.rb
@@ -41,16 +41,6 @@ RSpec.describe Gitlab::Ci::Build::Context::Build, feature_category: :pipeline_co
it { expect(context.variables).to be_instance_of(Gitlab::Ci::Variables::Collection) }
it_behaves_like 'variables collection'
-
- context 'with FF disabled' do
- before do
- stub_feature_flags(reduced_build_attributes_list_for_rules: false)
- end
-
- it { expect(context.variables).to be_instance_of(Gitlab::Ci::Variables::Collection) }
-
- it_behaves_like 'variables collection'
- end
end
describe '#variables_hash' do
@@ -59,15 +49,5 @@ RSpec.describe Gitlab::Ci::Build::Context::Build, feature_category: :pipeline_co
it { expect(context.variables_hash).to be_instance_of(ActiveSupport::HashWithIndifferentAccess) }
it_behaves_like 'variables collection'
-
- context 'with FF disabled' do
- before do
- stub_feature_flags(reduced_build_attributes_list_for_rules: false)
- end
-
- it { expect(context.variables_hash).to be_instance_of(ActiveSupport::HashWithIndifferentAccess) }
-
- it_behaves_like 'variables collection'
- end
end
end
diff --git a/spec/lib/gitlab/ci/build/hook_spec.rb b/spec/lib/gitlab/ci/build/hook_spec.rb
index 6c9175b4260..da9a680f110 100644
--- a/spec/lib/gitlab/ci/build/hook_spec.rb
+++ b/spec/lib/gitlab/ci/build/hook_spec.rb
@@ -4,8 +4,10 @@ require 'spec_helper'
RSpec.describe Gitlab::Ci::Build::Hook, feature_category: :pipeline_composition do
let_it_be(:build1) do
- FactoryBot.build(:ci_build,
- options: { hooks: { pre_get_sources_script: ["echo 'hello pre_get_sources_script'"] } })
+ build(
+ :ci_build,
+ options: { hooks: { pre_get_sources_script: ["echo 'hello pre_get_sources_script'"] } }
+ )
end
describe '.from_hooks' do
diff --git a/spec/lib/gitlab/ci/build/policy/changes_spec.rb b/spec/lib/gitlab/ci/build/policy/changes_spec.rb
index 5d5a212b9a5..00e44650d44 100644
--- a/spec/lib/gitlab/ci/build/policy/changes_spec.rb
+++ b/spec/lib/gitlab/ci/build/policy/changes_spec.rb
@@ -8,11 +8,14 @@ RSpec.describe Gitlab::Ci::Build::Policy::Changes do
describe '#satisfied_by?' do
describe 'paths matching' do
let(:pipeline) do
- build(:ci_empty_pipeline, project: project,
- ref: 'master',
- source: :push,
- sha: '1234abcd',
- before_sha: '0123aabb')
+ build(
+ :ci_empty_pipeline,
+ project: project,
+ ref: 'master',
+ source: :push,
+ sha: '1234abcd',
+ before_sha: '0123aabb'
+ )
end
let(:ci_build) do
@@ -92,11 +95,14 @@ RSpec.describe Gitlab::Ci::Build::Policy::Changes do
let_it_be(:project) { create(:project, :repository) }
let(:pipeline) do
- create(:ci_empty_pipeline, project: project,
- ref: 'master',
- source: :push,
- sha: '498214d',
- before_sha: '281d3a7')
+ create(
+ :ci_empty_pipeline,
+ project: project,
+ ref: 'master',
+ source: :push,
+ sha: '498214d',
+ before_sha: '281d3a7'
+ )
end
let(:build) do
@@ -122,12 +128,15 @@ RSpec.describe Gitlab::Ci::Build::Policy::Changes do
let_it_be(:project) { create(:project, :repository) }
let(:pipeline) do
- create(:ci_empty_pipeline, project: project,
- ref: 'feature',
- source: source,
- sha: '0b4bc9a4',
- before_sha: Gitlab::Git::BLANK_SHA,
- merge_request: merge_request)
+ create(
+ :ci_empty_pipeline,
+ project: project,
+ ref: 'feature',
+ source: source,
+ sha: '0b4bc9a4',
+ before_sha: Gitlab::Git::BLANK_SHA,
+ merge_request: merge_request
+ )
end
let(:ci_build) do
@@ -140,11 +149,13 @@ RSpec.describe Gitlab::Ci::Build::Policy::Changes do
let(:source) { :merge_request_event }
let(:merge_request) do
- create(:merge_request,
- source_project: project,
- source_branch: 'feature',
- target_project: project,
- target_branch: 'master')
+ create(
+ :merge_request,
+ source_project: project,
+ source_branch: 'feature',
+ target_project: project,
+ target_branch: 'master'
+ )
end
it 'is satified by changes in the merge request' do
diff --git a/spec/lib/gitlab/ci/build/policy/variables_spec.rb b/spec/lib/gitlab/ci/build/policy/variables_spec.rb
index e560f1c2b5a..1073df60d4a 100644
--- a/spec/lib/gitlab/ci/build/policy/variables_spec.rb
+++ b/spec/lib/gitlab/ci/build/policy/variables_spec.rb
@@ -104,23 +104,32 @@ RSpec.describe Gitlab::Ci::Build::Policy::Variables do
context 'when using project ci variables in environment scope' do
let(:ci_build) do
- build(:ci_build, pipeline: pipeline,
- project: project,
- ref: 'master',
- stage: 'review',
- environment: 'test/$CI_JOB_STAGE/1',
- ci_stage: build(:ci_stage, name: 'review', project: project, pipeline: pipeline))
+ build(
+ :ci_build,
+ pipeline: pipeline,
+ project: project,
+ ref: 'master',
+ stage: 'review',
+ environment: 'test/$CI_JOB_STAGE/1',
+ ci_stage: build(:ci_stage, name: 'review', project: project, pipeline: pipeline)
+ )
end
before do
- create(:ci_variable, project: project,
- key: 'SCOPED_VARIABLE',
- value: 'my-value-1')
-
- create(:ci_variable, project: project,
- key: 'SCOPED_VARIABLE',
- value: 'my-value-2',
- environment_scope: 'test/review/*')
+ create(
+ :ci_variable,
+ project: project,
+ key: 'SCOPED_VARIABLE',
+ value: 'my-value-1'
+ )
+
+ create(
+ :ci_variable,
+ project: project,
+ key: 'SCOPED_VARIABLE',
+ value: 'my-value-2',
+ environment_scope: 'test/review/*'
+ )
end
it 'is satisfied by scoped variable match' do
diff --git a/spec/lib/gitlab/ci/build/rules/rule/clause/changes_spec.rb b/spec/lib/gitlab/ci/build/rules/rule/clause/changes_spec.rb
index a22aa30304b..1fe54b9f2d5 100644
--- a/spec/lib/gitlab/ci/build/rules/rule/clause/changes_spec.rb
+++ b/spec/lib/gitlab/ci/build/rules/rule/clause/changes_spec.rb
@@ -95,8 +95,11 @@ RSpec.describe Gitlab::Ci::Build::Rules::Rule::Clause::Changes do
context 'when using compare_to' do
let_it_be(:project) do
- create(:project, :custom_repo,
- files: { 'README.md' => 'readme' })
+ create(
+ :project,
+ :custom_repo,
+ files: { 'README.md' => 'readme' }
+ )
end
let_it_be(:user) { project.owner }
diff --git a/spec/lib/gitlab/ci/config/entry/bridge_spec.rb b/spec/lib/gitlab/ci/config/entry/bridge_spec.rb
index 567ffa68836..6e6b9d949c5 100644
--- a/spec/lib/gitlab/ci/config/entry/bridge_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/bridge_spec.rb
@@ -101,14 +101,16 @@ RSpec.describe Gitlab::Ci::Config::Entry::Bridge do
describe '#value' do
it 'is returns a bridge job configuration' do
- expect(subject.value).to eq(name: :my_bridge,
- trigger: { project: 'some/project' },
- ignore: false,
- stage: 'test',
- only: { refs: %w[branches tags] },
- job_variables: {},
- root_variables_inheritance: true,
- scheduling_type: :stage)
+ expect(subject.value).to eq(
+ name: :my_bridge,
+ trigger: { project: 'some/project' },
+ ignore: false,
+ stage: 'test',
+ only: { refs: %w[branches tags] },
+ job_variables: {},
+ root_variables_inheritance: true,
+ scheduling_type: :stage
+ )
end
end
end
@@ -124,15 +126,16 @@ RSpec.describe Gitlab::Ci::Config::Entry::Bridge do
describe '#value' do
it 'is returns a bridge job configuration hash' do
- expect(subject.value).to eq(name: :my_bridge,
- trigger: { project: 'some/project',
- branch: 'feature' },
- ignore: false,
- stage: 'test',
- only: { refs: %w[branches tags] },
- job_variables: {},
- root_variables_inheritance: true,
- scheduling_type: :stage)
+ expect(subject.value).to eq(
+ name: :my_bridge,
+ trigger: { project: 'some/project', branch: 'feature' },
+ ignore: false,
+ stage: 'test',
+ only: { refs: %w[branches tags] },
+ job_variables: {},
+ root_variables_inheritance: true,
+ scheduling_type: :stage
+ )
end
end
end
@@ -283,8 +286,8 @@ RSpec.describe Gitlab::Ci::Config::Entry::Bridge do
ignore: false,
stage: 'test',
only: { refs: %w[branches tags] },
- parallel: { matrix: [{ 'PROVIDER' => ['aws'], 'STACK' => %w(monitoring app1) },
- { 'PROVIDER' => ['gcp'], 'STACK' => %w(data) }] },
+ parallel: { matrix: [{ 'PROVIDER' => ['aws'], 'STACK' => %w[monitoring app1] },
+ { 'PROVIDER' => ['gcp'], 'STACK' => %w[data] }] },
job_variables: {},
root_variables_inheritance: true,
scheduling_type: :stage
@@ -305,15 +308,16 @@ RSpec.describe Gitlab::Ci::Config::Entry::Bridge do
describe '#value' do
it 'returns a bridge job configuration hash' do
- expect(subject.value).to eq(name: :my_bridge,
- trigger: { project: 'some/project',
- forward: { pipeline_variables: true } },
- ignore: false,
- stage: 'test',
- only: { refs: %w[branches tags] },
- job_variables: {},
- root_variables_inheritance: true,
- scheduling_type: :stage)
+ expect(subject.value).to eq(
+ name: :my_bridge,
+ trigger: { project: 'some/project', forward: { pipeline_variables: true } },
+ ignore: false,
+ stage: 'test',
+ only: { refs: %w[branches tags] },
+ job_variables: {},
+ root_variables_inheritance: true,
+ scheduling_type: :stage
+ )
end
end
end
diff --git a/spec/lib/gitlab/ci/config/entry/commands_spec.rb b/spec/lib/gitlab/ci/config/entry/commands_spec.rb
index 1b8dfae692a..f84a78b4804 100644
--- a/spec/lib/gitlab/ci/config/entry/commands_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/commands_spec.rb
@@ -6,7 +6,7 @@ RSpec.describe Gitlab::Ci::Config::Entry::Commands do
let(:entry) { described_class.new(config) }
context 'when entry config value is an array of strings' do
- let(:config) { %w(ls pwd) }
+ let(:config) { %w[ls pwd] }
describe '#value' do
it 'returns array of strings' do
diff --git a/spec/lib/gitlab/ci/config/entry/environment_spec.rb b/spec/lib/gitlab/ci/config/entry/environment_spec.rb
index 3562706ff33..cff94a96c99 100644
--- a/spec/lib/gitlab/ci/config/entry/environment_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/environment_spec.rb
@@ -93,7 +93,7 @@ RSpec.describe Gitlab::Ci::Config::Entry::Environment do
context 'when valid action is used' do
where(:action) do
- %w(start stop prepare verify access)
+ %w[start stop prepare verify access]
end
with_them do
diff --git a/spec/lib/gitlab/ci/config/entry/image_spec.rb b/spec/lib/gitlab/ci/config/entry/image_spec.rb
index b37498ba10a..17c45ec4c2c 100644
--- a/spec/lib/gitlab/ci/config/entry/image_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/image_spec.rb
@@ -56,7 +56,7 @@ RSpec.describe Gitlab::Ci::Config::Entry::Image do
end
context 'when configuration is a hash' do
- let(:config) { { name: 'image:1.0', entrypoint: %w(/bin/sh run) } }
+ let(:config) { { name: 'image:1.0', entrypoint: %w[/bin/sh run] } }
describe '#value' do
it 'returns image hash' do
@@ -84,13 +84,13 @@ RSpec.describe Gitlab::Ci::Config::Entry::Image do
describe '#entrypoint' do
it "returns image's entrypoint" do
- expect(entry.entrypoint).to eq %w(/bin/sh run)
+ expect(entry.entrypoint).to eq %w[/bin/sh run]
end
end
context 'when configuration has ports' do
let(:ports) { [{ number: 80, protocol: 'http', name: 'foobar' }] }
- let(:config) { { name: 'image:1.0', entrypoint: %w(/bin/sh run), ports: ports } }
+ let(:config) { { name: 'image:1.0', entrypoint: %w[/bin/sh run], ports: ports } }
let(:entry) { described_class.new(config, with_image_ports: image_ports) }
let(:image_ports) { false }
diff --git a/spec/lib/gitlab/ci/config/entry/job_spec.rb b/spec/lib/gitlab/ci/config/entry/job_spec.rb
index 1a78d929871..24d3cac6616 100644
--- a/spec/lib/gitlab/ci/config/entry/job_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/job_spec.rb
@@ -598,7 +598,7 @@ RSpec.describe Gitlab::Ci::Config::Entry::Job, feature_category: :pipeline_compo
end
end
- context 'when job is not a pages job' do
+ context 'when job is not a pages job', feature_category: :pages do
let(:name) { :rspec }
context 'if the config contains a publish entry' do
@@ -609,9 +609,18 @@ RSpec.describe Gitlab::Ci::Config::Entry::Job, feature_category: :pipeline_compo
expect(entry.errors).to include /job publish can only be used within a `pages` job/
end
end
+
+ context 'if the config contains a pages entry' do
+ let(:entry) { described_class.new({ script: 'echo', pages: { path_prefix: 'foo' } }, name: name) }
+
+ it 'is invalid' do
+ expect(entry).not_to be_valid
+ expect(entry.errors).to include /job pages can only be used within a `pages` job/
+ end
+ end
end
- context 'when job is a pages job' do
+ context 'when job is a pages job', feature_category: :pages do
let(:name) { :pages }
context 'when it does not have a publish entry' do
@@ -629,6 +638,28 @@ RSpec.describe Gitlab::Ci::Config::Entry::Job, feature_category: :pipeline_compo
expect(entry).to be_valid
end
end
+
+ context 'when it has a pages entry' do
+ let(:entry) { described_class.new({ script: 'echo', pages: { path_prefix: 'foo' } }, name: name) }
+
+ it 'is valid' do
+ expect(entry).to be_valid
+ end
+ end
+ end
+ end
+
+ describe '#pages_job?', :aggregate_failures, feature_category: :pages do
+ where(:name, :result) do
+ :pages | true
+ :'pages:staging' | false
+ :'something:pages:else' | false
+ end
+
+ with_them do
+ subject { described_class.new({}, name: name).pages_job? }
+
+ it { is_expected.to eq(result) }
end
end
@@ -739,20 +770,6 @@ RSpec.describe Gitlab::Ci::Config::Entry::Job, feature_category: :pipeline_compo
end
end
- describe '#pages_job?', :aggregate_failures, feature_category: :pages do
- where(:name, :result) do
- :pages | true
- :'pages:staging' | false
- :'something:pages:else' | false
- end
-
- with_them do
- subject { described_class.new({}, name: name).pages_job? }
-
- it { is_expected.to eq(result) }
- end
- end
-
context 'when composed' do
before do
entry.compose!
@@ -773,19 +790,20 @@ RSpec.describe Gitlab::Ci::Config::Entry::Job, feature_category: :pipeline_compo
end
it 'returns correct value' do
- expect(entry.value)
- .to eq(name: :rspec,
- before_script: %w[ls pwd],
- script: %w[rspec],
- stage: 'test',
- ignore: false,
- after_script: %w[cleanup],
- hooks: { pre_get_sources_script: ['echo hello'] },
- only: { refs: %w[branches tags] },
- job_variables: {},
- root_variables_inheritance: true,
- scheduling_type: :stage,
- id_tokens: { TEST_ID_TOKEN: { aud: 'https://gitlab.com' } })
+ expect(entry.value).to eq(
+ name: :rspec,
+ before_script: %w[ls pwd],
+ script: %w[rspec],
+ stage: 'test',
+ ignore: false,
+ after_script: %w[cleanup],
+ hooks: { pre_get_sources_script: ['echo hello'] },
+ only: { refs: %w[branches tags] },
+ job_variables: {},
+ root_variables_inheritance: true,
+ scheduling_type: :stage,
+ id_tokens: { TEST_ID_TOKEN: { aud: 'https://gitlab.com' } }
+ )
end
end
end
diff --git a/spec/lib/gitlab/ci/config/entry/pages_spec.rb b/spec/lib/gitlab/ci/config/entry/pages_spec.rb
new file mode 100644
index 00000000000..0ee692a443e
--- /dev/null
+++ b/spec/lib/gitlab/ci/config/entry/pages_spec.rb
@@ -0,0 +1,49 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Ci::Config::Entry::Pages, feature_category: :pages do
+ subject(:entry) { described_class.new(config) }
+
+ describe 'validation' do
+ context 'when value given is not a hash' do
+ let(:config) { 'value' }
+
+ it 'is invalid' do
+ expect(entry).not_to be_valid
+ expect(entry.errors).to include('pages config should be a hash')
+ end
+ end
+
+ context 'when value is a hash' do
+ context 'when the hash is valid' do
+ let(:config) { { path_prefix: 'prefix' } }
+
+ it 'is valid' do
+ expect(entry).to be_valid
+ expect(entry.value).to eq({
+ path_prefix: 'prefix'
+ })
+ end
+ end
+
+ context 'when path_prefix key is not a string' do
+ let(:config) { { path_prefix: 1 } }
+
+ it 'is invalid' do
+ expect(entry).not_to be_valid
+ expect(entry.errors).to include('pages path prefix should be a string')
+ end
+ end
+
+ context 'when hash contains not allowed keys' do
+ let(:config) { { unknown: 'echo' } }
+
+ it 'is invalid' do
+ expect(entry).not_to be_valid
+ expect(entry.errors).to include('pages config contains unknown keys: unknown')
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/config/entry/policy_spec.rb b/spec/lib/gitlab/ci/config/entry/policy_spec.rb
index 7093a0a6edf..77a895b75c0 100644
--- a/spec/lib/gitlab/ci/config/entry/policy_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/policy_spec.rb
@@ -221,8 +221,7 @@ RSpec.describe Gitlab::Ci::Config::Entry::Policy, feature_category: :continuous_
let(:config) { { variables: %w[$VARIABLE] } }
it 'includes default values' do
- expect(entry.value).to eq(refs: %w[branches tags],
- variables: %w[$VARIABLE])
+ expect(entry.value).to eq(refs: %w[branches tags], variables: %w[$VARIABLE])
end
end
end
diff --git a/spec/lib/gitlab/ci/config/entry/processable_spec.rb b/spec/lib/gitlab/ci/config/entry/processable_spec.rb
index 132e75a808b..44e2fdbac37 100644
--- a/spec/lib/gitlab/ci/config/entry/processable_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/processable_spec.rb
@@ -119,6 +119,20 @@ RSpec.describe Gitlab::Ci::Config::Entry::Processable, feature_category: :pipeli
end
end
+ context 'when script: and trigger: are used together' do
+ let(:config) do
+ {
+ script: 'echo',
+ trigger: 'test-group/test-project'
+ }
+ end
+
+ it 'returns is invalid' do
+ expect(entry).not_to be_valid
+ expect(entry.errors).to include(/these keys cannot be used together: script, trigger/)
+ end
+ end
+
context 'when only: is used with rules:' do
let(:config) { { only: ['merge_requests'], rules: [{ if: '$THIS' }] } }
diff --git a/spec/lib/gitlab/ci/config/entry/root_spec.rb b/spec/lib/gitlab/ci/config/entry/root_spec.rb
index 5fac5298e8e..0370bcbccf5 100644
--- a/spec/lib/gitlab/ci/config/entry/root_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/root_spec.rb
@@ -31,7 +31,7 @@ RSpec.describe Gitlab::Ci::Config::Entry::Root do
context 'when top-level entries are defined' do
let(:hash) do
{
- before_script: %w(ls pwd),
+ before_script: %w[ls pwd],
image: 'image:1.0',
default: {},
services: ['postgres:9.1', 'mysql:5.5'],
@@ -41,7 +41,7 @@ RSpec.describe Gitlab::Ci::Config::Entry::Root do
VAR3: { value: 'val3', options: %w[val3 val4 val5], description: 'this is var 3 and some options' }
},
after_script: ['make clean'],
- stages: %w(build pages release),
+ stages: %w[build pages release],
cache: { key: 'k', untracked: true, paths: ['public/'] },
rspec: { script: %w[rspec ls] },
spinach: { before_script: [], variables: {}, script: 'spinach' },
@@ -123,7 +123,7 @@ RSpec.describe Gitlab::Ci::Config::Entry::Root do
expect(root.jobs_value[:rspec]).to eq(
{ name: :rspec,
script: %w[rspec ls],
- before_script: %w(ls pwd),
+ before_script: %w[ls pwd],
image: { name: 'image:1.0' },
services: [{ name: 'postgres:9.1' }, { name: 'mysql:5.5' }],
stage: 'test',
@@ -162,7 +162,7 @@ RSpec.describe Gitlab::Ci::Config::Entry::Root do
services: [{ name: "postgres:9.1" }, { name: "mysql:5.5" }],
cache: [{ key: "k", untracked: true, paths: ["public/"], policy: "pull-push", when: 'on_success',
unprotect: false, fallback_keys: [] }],
- only: { refs: %w(branches tags) },
+ only: { refs: %w[branches tags] },
job_variables: { 'VAR' => { value: 'job' } },
root_variables_inheritance: true,
after_script: [],
@@ -176,14 +176,14 @@ RSpec.describe Gitlab::Ci::Config::Entry::Root do
context 'when a mix of top-level and default entries is used' do
let(:hash) do
- { before_script: %w(ls pwd),
+ { before_script: %w[ls pwd],
after_script: ['make clean'],
default: {
image: 'image:1.0',
services: ['postgres:9.1', 'mysql:5.5']
},
variables: { VAR: 'root' },
- stages: %w(build pages),
+ stages: %w[build pages],
cache: { key: 'k', untracked: true, paths: ['public/'] },
rspec: { script: %w[rspec ls] },
spinach: { before_script: [], variables: { VAR: 'job' }, script: 'spinach' } }
@@ -205,7 +205,7 @@ RSpec.describe Gitlab::Ci::Config::Entry::Root do
expect(root.jobs_value).to eq(
rspec: { name: :rspec,
script: %w[rspec ls],
- before_script: %w(ls pwd),
+ before_script: %w[ls pwd],
image: { name: 'image:1.0' },
services: [{ name: 'postgres:9.1' }, { name: 'mysql:5.5' }],
stage: 'test',
diff --git a/spec/lib/gitlab/ci/config/entry/service_spec.rb b/spec/lib/gitlab/ci/config/entry/service_spec.rb
index e36484bb0ae..1f935bebed5 100644
--- a/spec/lib/gitlab/ci/config/entry/service_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/service_spec.rb
@@ -51,7 +51,7 @@ RSpec.describe Gitlab::Ci::Config::Entry::Service do
context 'when configuration is a hash' do
let(:config) do
- { name: 'postgresql:9.5', alias: 'db', command: %w(cmd run), entrypoint: %w(/bin/sh run) }
+ { name: 'postgresql:9.5', alias: 'db', command: %w[cmd run], entrypoint: %w[/bin/sh run] }
end
describe '#valid?' do
@@ -80,13 +80,13 @@ RSpec.describe Gitlab::Ci::Config::Entry::Service do
describe '#command' do
it "returns service's command" do
- expect(entry.command).to eq %w(cmd run)
+ expect(entry.command).to eq %w[cmd run]
end
end
describe '#entrypoint' do
it "returns service's entrypoint" do
- expect(entry.entrypoint).to eq %w(/bin/sh run)
+ expect(entry.entrypoint).to eq %w[/bin/sh run]
end
end
@@ -99,7 +99,7 @@ RSpec.describe Gitlab::Ci::Config::Entry::Service do
context 'when configuration has ports' do
let(:ports) { [{ number: 80, protocol: 'http', name: 'foobar' }] }
let(:config) do
- { name: 'postgresql:9.5', alias: 'db', command: %w(cmd run), entrypoint: %w(/bin/sh run), ports: ports }
+ { name: 'postgresql:9.5', alias: 'db', command: %w[cmd run], entrypoint: %w[/bin/sh run], ports: ports }
end
let(:entry) { described_class.new(config, with_image_ports: image_ports) }
@@ -198,7 +198,7 @@ RSpec.describe Gitlab::Ci::Config::Entry::Service do
context 'when service has ports' do
let(:ports) { [{ number: 80, protocol: 'http', name: 'foobar' }] }
let(:config) do
- { name: 'postgresql:9.5', command: %w(cmd run), entrypoint: %w(/bin/sh run), ports: ports }
+ { name: 'postgresql:9.5', command: %w[cmd run], entrypoint: %w[/bin/sh run], ports: ports }
end
it 'alias field is mandatory' do
@@ -209,7 +209,7 @@ RSpec.describe Gitlab::Ci::Config::Entry::Service do
context 'when service does not have ports' do
let(:config) do
- { name: 'postgresql:9.5', alias: 'db', command: %w(cmd run), entrypoint: %w(/bin/sh run) }
+ { name: 'postgresql:9.5', alias: 'db', command: %w[cmd run], entrypoint: %w[/bin/sh run] }
end
it 'alias field is optional' do
diff --git a/spec/lib/gitlab/ci/config/extendable/entry_spec.rb b/spec/lib/gitlab/ci/config/extendable/entry_spec.rb
index 69aa3bab77a..b57a56b8389 100644
--- a/spec/lib/gitlab/ci/config/extendable/entry_spec.rb
+++ b/spec/lib/gitlab/ci/config/extendable/entry_spec.rb
@@ -128,8 +128,7 @@ RSpec.describe Gitlab::Ci::Config::Extendable::Entry do
it 'raises an error' do
expect { subject.extend! }
- .to raise_error(described_class::InvalidExtensionError,
- /invalid base hash/)
+ .to raise_error(described_class::InvalidExtensionError, /invalid base hash/)
end
end
@@ -140,8 +139,7 @@ RSpec.describe Gitlab::Ci::Config::Extendable::Entry do
it 'raises an error' do
expect { subject.extend! }
- .to raise_error(described_class::InvalidExtensionError,
- /unknown key/)
+ .to raise_error(described_class::InvalidExtensionError, /unknown key/)
end
end
@@ -178,7 +176,7 @@ RSpec.describe Gitlab::Ci::Config::Extendable::Entry do
{
first: { script: 'my value', image: 'ubuntu' },
second: { image: 'alpine' },
- test: { extends: %w(first second) }
+ test: { extends: %w[first second] }
}
end
@@ -186,7 +184,7 @@ RSpec.describe Gitlab::Ci::Config::Extendable::Entry do
{
first: { script: 'my value', image: 'ubuntu' },
second: { image: 'alpine' },
- test: { extends: %w(first second), script: 'my value', image: 'alpine' }
+ test: { extends: %w[first second], script: 'my value', image: 'alpine' }
}
end
@@ -230,8 +228,7 @@ RSpec.describe Gitlab::Ci::Config::Extendable::Entry do
it 'raises an error' do
expect { subject.extend! }
- .to raise_error(described_class::CircularDependencyError,
- /circular dependency detected/)
+ .to raise_error(described_class::CircularDependencyError, /circular dependency detected/)
end
end
diff --git a/spec/lib/gitlab/ci/config/external/file/base_spec.rb b/spec/lib/gitlab/ci/config/external/file/base_spec.rb
index 1415dbeb532..bcfab620bd9 100644
--- a/spec/lib/gitlab/ci/config/external/file/base_spec.rb
+++ b/spec/lib/gitlab/ci/config/external/file/base_spec.rb
@@ -60,7 +60,7 @@ RSpec.describe Gitlab::Ci::Config::External::File::Base, feature_category: :pipe
end
context 'when location is not a string' do
- let(:location) { %w(some/file.txt other/file.txt) }
+ let(:location) { %w[some/file.txt other/file.txt] }
it { is_expected.to be_falsy }
end
diff --git a/spec/lib/gitlab/ci/config/external/mapper_spec.rb b/spec/lib/gitlab/ci/config/external/mapper_spec.rb
index 56d1ddee4b8..5f28b45496f 100644
--- a/spec/lib/gitlab/ci/config/external/mapper_spec.rb
+++ b/spec/lib/gitlab/ci/config/external/mapper_spec.rb
@@ -406,8 +406,10 @@ RSpec.describe Gitlab::Ci::Config::External::Mapper, feature_category: :pipeline
end
it 'includes the matched local files' do
- expect(subject).to contain_exactly(an_instance_of(Gitlab::Ci::Config::External::File::Local),
- an_instance_of(Gitlab::Ci::Config::External::File::Local))
+ expect(subject).to contain_exactly(
+ an_instance_of(Gitlab::Ci::Config::External::File::Local),
+ an_instance_of(Gitlab::Ci::Config::External::File::Local)
+ )
expect(subject.map(&:location)).to contain_exactly('myfolder/file1.yml', 'myfolder/file2.yml')
end
@@ -424,8 +426,10 @@ RSpec.describe Gitlab::Ci::Config::External::Mapper, feature_category: :pipeline
let(:project_id) { project.id }
it 'includes the file' do
- expect(subject).to contain_exactly(an_instance_of(Gitlab::Ci::Config::External::File::Remote),
- an_instance_of(Gitlab::Ci::Config::External::File::Local))
+ expect(subject).to contain_exactly(
+ an_instance_of(Gitlab::Ci::Config::External::File::Remote),
+ an_instance_of(Gitlab::Ci::Config::External::File::Local)
+ )
end
end
diff --git a/spec/lib/gitlab/ci/config/header/input_spec.rb b/spec/lib/gitlab/ci/config/header/input_spec.rb
index 5d1fa4a8e6e..df70d1fd7c8 100644
--- a/spec/lib/gitlab/ci/config/header/input_spec.rb
+++ b/spec/lib/gitlab/ci/config/header/input_spec.rb
@@ -40,12 +40,24 @@ RSpec.describe Gitlab::Ci::Config::Header::Input, feature_category: :pipeline_co
end
end
- context 'when has a default value' do
+ context 'when has a string default value' do
let(:input_hash) { { default: 'bar' } }
it_behaves_like 'a valid input'
end
+ context 'when has a numeric default value' do
+ let(:input_hash) { { default: 6.66 } }
+
+ it_behaves_like 'a valid input'
+ end
+
+ context 'when has a boolean default value' do
+ let(:input_hash) { { default: true } }
+
+ it_behaves_like 'a valid input'
+ end
+
context 'when has a description value' do
let(:input_hash) { { description: 'bar' } }
@@ -103,4 +115,21 @@ RSpec.describe Gitlab::Ci::Config::Header::Input, feature_category: :pipeline_co
it_behaves_like 'an invalid input'
end
+
+ context 'when the limit for allowed number of options is reached' do
+ let(:limit) { described_class::ALLOWED_OPTIONS_LIMIT }
+ let(:input_hash) { { default: 'value1', options: options } }
+ let(:options) { Array.new(limit.next) { |i| "value#{i}" } }
+
+ describe '#valid?' do
+ it { is_expected.not_to be_valid }
+ end
+
+ describe '#errors' do
+ it 'returns error about incorrect type' do
+ expect(config.errors).to contain_exactly(
+ "foo config cannot define more than #{limit} options")
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/ci/config/interpolation/inputs_spec.rb b/spec/lib/gitlab/ci/config/interpolation/inputs_spec.rb
index b0618081207..57ced4eab98 100644
--- a/spec/lib/gitlab/ci/config/interpolation/inputs_spec.rb
+++ b/spec/lib/gitlab/ci/config/interpolation/inputs_spec.rb
@@ -7,6 +7,90 @@ RSpec.describe Gitlab::Ci::Config::Interpolation::Inputs, feature_category: :pip
let(:specs) { { foo: { default: 'bar' } } }
let(:args) { {} }
+ context 'when inputs are valid strings and have options' do
+ let(:specs) { { foo: { default: 'one', options: %w[one two three] } } }
+
+ context 'and the value is selected' do
+ let(:args) { { foo: 'two' } }
+
+ it 'assigns the selected value' do
+ expect(inputs).to be_valid
+ expect(inputs.to_hash).to eq({ foo: 'two' })
+ end
+ end
+
+ context 'and the value is not selected' do
+ it 'assigns the default value' do
+ expect(inputs).to be_valid
+ expect(inputs.to_hash).to eq({ foo: 'one' })
+ end
+ end
+ end
+
+ context 'when inputs options are valid integers' do
+ let(:specs) { { foo: { default: 1, options: [1, 2, 3, 4, 5], type: 'number' } } }
+
+ context 'and a value of the wrong type is given' do
+ let(:args) { { foo: 'word' } }
+
+ it 'returns an error' do
+ expect(inputs).not_to be_valid
+ expect(inputs.errors).to contain_exactly(
+ "`foo` input: `word` cannot be used because it is not in the list of the allowed options",
+ "`foo` input: provided value is not a number"
+ )
+ end
+ end
+
+ context 'and the value is selected' do
+ let(:args) { { foo: 2 } }
+
+ it 'assigns the selected value' do
+ expect(inputs).to be_valid
+ expect(inputs.to_hash).to eq({ foo: 2 })
+ end
+ end
+
+ context 'and the value is not selected' do
+ it 'assigns the default value' do
+ expect(inputs).to be_valid
+ expect(inputs.to_hash).to eq({ foo: 1 })
+ end
+ end
+ end
+
+ context 'when inputs have invalid type options' do
+ let(:specs) { { foo: { default: true, options: [true, false], type: 'boolean' } } }
+
+ it 'returns an error' do
+ expect(inputs).not_to be_valid
+ expect(inputs.errors).to contain_exactly("`foo` input: Options can only be used with string and number inputs")
+ end
+ end
+
+ context 'when inputs are valid with options but the default value is not in the options' do
+ let(:specs) { { foo: { default: 'coop', options: %w[one two three] } } }
+
+ it 'returns an error' do
+ expect(inputs).not_to be_valid
+ expect(inputs.errors).to contain_exactly(
+ '`foo` input: `coop` cannot be used because it is not in the list of allowed options'
+ )
+ end
+ end
+
+ context 'when inputs are valid with options but the value is not in the options' do
+ let(:specs) { { foo: { default: 'one', options: %w[one two three] } } }
+ let(:args) { { foo: 'niet' } }
+
+ it 'returns an error' do
+ expect(inputs).not_to be_valid
+ expect(inputs.errors).to contain_exactly(
+ '`foo` input: `niet` cannot be used because it is not in the list of allowed options'
+ )
+ end
+ end
+
context 'when given unrecognized inputs' do
let(:specs) { { foo: nil } }
let(:args) { { foo: 'bar', test: 'bar' } }
@@ -164,7 +248,7 @@ RSpec.describe Gitlab::Ci::Config::Interpolation::Inputs, feature_category: :pip
context 'when the value is not a number' do
let(:specs) { { number_input: { type: 'number' } } }
- let(:args) { { number_input: 'NaN' } }
+ let(:args) { { number_input: false } }
it 'is invalid' do
expect(inputs).not_to be_valid
diff --git a/spec/lib/gitlab/ci/jwt_v2_spec.rb b/spec/lib/gitlab/ci/jwt_v2_spec.rb
index d45d8cacb88..c2ced10620b 100644
--- a/spec/lib/gitlab/ci/jwt_v2_spec.rb
+++ b/spec/lib/gitlab/ci/jwt_v2_spec.rb
@@ -33,14 +33,6 @@ RSpec.describe Gitlab::Ci::JwtV2, feature_category: :continuous_integration do
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
-
it 'includes user identities when enabled' do
expect(user).to receive(:pass_user_identities_to_ci_jwt).and_return(true)
identities = payload[:user_identities].map { |identity| identity.slice(:extern_uid, :provider) }
@@ -53,6 +45,34 @@ RSpec.describe Gitlab::Ci::JwtV2, feature_category: :continuous_integration do
expect(payload).not_to include(:user_identities)
end
+ context 'when oidc_issuer_url is disabled' do
+ before do
+ stub_feature_flags(oidc_issuer_url: false)
+ end
+
+ 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
+
+ context 'when oidc_issuer_url is enabled' do
+ before do
+ stub_feature_flags(oidc_issuer_url: true)
+ end
+
+ it 'has correct values for the standard JWT attributes' do
+ aggregate_failures do
+ expect(payload[:iss]).to eq(Gitlab.config.gitlab.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
+
context 'when given an aud' do
let(:aud) { 'AWS' }
diff --git a/spec/lib/gitlab/ci/parsers/sbom/cyclonedx_properties_spec.rb b/spec/lib/gitlab/ci/parsers/sbom/cyclonedx_properties_spec.rb
index dacbe07c8b3..2c57106b07c 100644
--- a/spec/lib/gitlab/ci/parsers/sbom/cyclonedx_properties_spec.rb
+++ b/spec/lib/gitlab/ci/parsers/sbom/cyclonedx_properties_spec.rb
@@ -42,15 +42,16 @@ RSpec.describe Gitlab::Ci::Parsers::Sbom::CyclonedxProperties, feature_category:
it { is_expected.to be_nil }
end
- context 'when no dependency_scanning properties are present' do
+ context 'when no dependency_scanning or container_scanning properties are present' do
let(:properties) do
[
{ 'name' => 'gitlab:meta:schema_version', 'value' => '1' }
]
end
- it 'does not call dependency_scanning parser' do
+ it 'does not call source parsers' do
expect(Gitlab::Ci::Parsers::Sbom::Source::DependencyScanning).not_to receive(:source)
+ expect(Gitlab::Ci::Parsers::Sbom::Source::ContainerScanning).not_to receive(:source)
parse_source_from_properties
end
@@ -85,4 +86,35 @@ RSpec.describe Gitlab::Ci::Parsers::Sbom::CyclonedxProperties, feature_category:
parse_source_from_properties
end
end
+
+ context 'when container_scanning properties are present' do
+ let(:properties) do
+ [
+ { 'name' => 'gitlab:meta:schema_version', 'value' => '1' },
+ { 'name' => 'gitlab:container_scanning:image:name', 'value' => 'photon' },
+ { 'name' => 'gitlab:container_scanning:image:tag', 'value' => '5.0-20231007' },
+ { 'name' => 'gitlab:container_scanning:operating_system:name', 'value' => 'Photon OS' },
+ { 'name' => 'gitlab:container_scanning:operating_system:version', 'value' => '5.0' }
+ ]
+ end
+
+ let(:expected_input) do
+ {
+ 'image' => {
+ 'name' => 'photon',
+ 'tag' => '5.0-20231007'
+ },
+ 'operating_system' => {
+ 'name' => 'Photon OS',
+ 'version' => '5.0'
+ }
+ }
+ end
+
+ it 'passes only supported properties to the container scanning parser' do
+ expect(Gitlab::Ci::Parsers::Sbom::Source::ContainerScanning).to receive(:source).with(expected_input)
+
+ parse_source_from_properties
+ end
+ end
end
diff --git a/spec/lib/gitlab/ci/parsers/sbom/source/container_scanning_spec.rb b/spec/lib/gitlab/ci/parsers/sbom/source/container_scanning_spec.rb
new file mode 100644
index 00000000000..410b2c0098a
--- /dev/null
+++ b/spec/lib/gitlab/ci/parsers/sbom/source/container_scanning_spec.rb
@@ -0,0 +1,58 @@
+# frozen_string_literal: true
+
+require 'fast_spec_helper'
+
+RSpec.describe Gitlab::Ci::Parsers::Sbom::Source::ContainerScanning, feature_category: :container_scanning do
+ subject { described_class.source(property_data) }
+
+ context 'when required properties are present' do
+ let(:property_data) do
+ {
+ 'image' => {
+ 'name' => 'photon',
+ 'tag' => '5.0-20231007'
+ },
+ 'operating_system' => {
+ 'name' => 'Photon OS',
+ 'version' => '5.0'
+ }
+ }
+ end
+
+ it 'returns expected source data' do
+ is_expected.to have_attributes(
+ source_type: :container_scanning,
+ data: property_data
+ )
+ end
+ end
+
+ context 'when required properties are missing' do
+ let(:property_data) do
+ {
+ 'operating_system' => {
+ 'name' => 'Photon OS',
+ 'version' => '5.0'
+ }
+ }
+ end
+
+ it { is_expected.to be_nil }
+ end
+
+ context 'when some operating_system properties are missing' do
+ let(:property_data) do
+ {
+ 'image' => {
+ 'name' => 'photon',
+ 'tag' => '5.0-20231007'
+ },
+ 'operating_system' => {
+ 'name' => 'Photon OS'
+ }
+ }
+ end
+
+ it { is_expected.to be_nil }
+ end
+end
diff --git a/spec/lib/gitlab/ci/parsers/security/common_spec.rb b/spec/lib/gitlab/ci/parsers/security/common_spec.rb
index 648b8ac2db9..431a6d94c48 100644
--- a/spec/lib/gitlab/ci/parsers/security/common_spec.rb
+++ b/spec/lib/gitlab/ci/parsers/security/common_spec.rb
@@ -335,7 +335,7 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Common, feature_category: :vulnera
expect(flags).to contain_exactly(
have_attributes(type: 'flagged-as-likely-false-positive', origin: 'post analyzer X', description: 'static string to sink'),
- have_attributes(type: 'flagged-as-likely-false-positive', origin: 'post analyzer Y', description: 'integer to sink')
+ have_attributes(type: 'flagged-as-likely-false-positive', origin: 'post analyzer Y', description: 'integer to sink')
)
end
end
diff --git a/spec/lib/gitlab/ci/parsers/security/secret_detection_spec.rb b/spec/lib/gitlab/ci/parsers/security/secret_detection_spec.rb
index 13999b2a9e5..640bed0d329 100644
--- a/spec/lib/gitlab/ci/parsers/security/secret_detection_spec.rb
+++ b/spec/lib/gitlab/ci/parsers/security/secret_detection_spec.rb
@@ -9,7 +9,7 @@ RSpec.describe Gitlab::Ci::Parsers::Security::SecretDetection do
let(:created_at) { 2.weeks.ago }
context "when parsing valid reports" do
- where(report_format: %i(secret_detection))
+ where(report_format: %i[secret_detection])
with_them do
let(:report) { Gitlab::Ci::Reports::Security::Report.new(artifact.file_type, pipeline, created_at) }
diff --git a/spec/lib/gitlab/ci/pipeline/chain/template_usage_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/template_usage_spec.rb
index ddd0de69d79..70d73a8095c 100644
--- a/spec/lib/gitlab/ci/pipeline/chain/template_usage_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/chain/template_usage_spec.rb
@@ -21,11 +21,11 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::TemplateUsage do
expect(command).to(
receive(:yaml_processor_result)
.and_return(
- double(included_templates: %w(Template-1 Template-2))
+ double(included_templates: %w[Template-1 Template-2])
)
)
- %w(Template-1 Template-2).each do |expected_template|
+ %w[Template-1 Template-2].each do |expected_template|
expect(Gitlab::UsageDataCounters::CiTemplateUniqueCounter).to(
receive(:track_unique_project_event)
.with(project: project, template: expected_template, config_source: pipeline.config_source, user: user)
diff --git a/spec/lib/gitlab/ci/pipeline/expression/lexeme/equals_spec.rb b/spec/lib/gitlab/ci/pipeline/expression/lexeme/equals_spec.rb
index ab223ae41fa..eb71cc0f0bc 100644
--- a/spec/lib/gitlab/ci/pipeline/expression/lexeme/equals_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/expression/lexeme/equals_spec.rb
@@ -49,7 +49,7 @@ RSpec.describe Gitlab::Ci::Pipeline::Expression::Lexeme::Equals do
context 'when left and right are equal' do
where(:left_value, :right_value) do
- [%w(string string)]
+ [%w[string string]]
end
with_them do
diff --git a/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb b/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb
index 54e569f424b..ef9b8f2b82f 100644
--- a/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb
@@ -395,7 +395,7 @@ RSpec.describe Gitlab::Ci::Pipeline::Seed::Build, feature_category: :pipeline_co
end
context 'when root_variables_inheritance is an array' do
- let(:root_variables_inheritance) { %w(VAR1 VAR2 VAR3) }
+ let(:root_variables_inheritance) { %w[VAR1 VAR2 VAR3] }
it 'returns calculated yaml variables' do
expect(subject[:yaml_variables]).to match_array(
diff --git a/spec/lib/gitlab/ci/reports/accessibility_reports_comparer_spec.rb b/spec/lib/gitlab/ci/reports/accessibility_reports_comparer_spec.rb
index ad8f1dc11f8..6d8b472a240 100644
--- a/spec/lib/gitlab/ci/reports/accessibility_reports_comparer_spec.rb
+++ b/spec/lib/gitlab/ci/reports/accessibility_reports_comparer_spec.rb
@@ -14,7 +14,7 @@ RSpec.describe Gitlab::Ci::Reports::AccessibilityReportsComparer do
"type" => "error",
"typeCode" => 1,
"message" => "Anchor element found with a valid href attribute, but no link content has been supplied.",
- "context" => %{<a href="/" class="navbar-brand animated"><svg height="36" viewBox="0 0 1...</a>},
+ "context" => %(<a href="/" class="navbar-brand animated"><svg height="36" viewBox="0 0 1...</a>),
"selector" => "#main-nav > div:nth-child(1) > a",
"runner" => "htmlcs",
"runnerExtras" => {}
@@ -29,7 +29,7 @@ RSpec.describe Gitlab::Ci::Reports::AccessibilityReportsComparer do
"type" => "error",
"typeCode" => 1,
"message" => "This element has insufficient contrast at this conformance level.",
- "context" => %{<a href="/stages-devops-lifecycle/" class="main-nav-link">Product</a>},
+ "context" => %(<a href="/stages-devops-lifecycle/" class="main-nav-link">Product</a>),
"selector" => "#main-nav > div:nth-child(2) > ul > li:nth-child(1) > a",
"runner" => "htmlcs",
"runnerExtras" => {}
diff --git a/spec/lib/gitlab/ci/reports/accessibility_reports_spec.rb b/spec/lib/gitlab/ci/reports/accessibility_reports_spec.rb
index af6844491ca..dff59474746 100644
--- a/spec/lib/gitlab/ci/reports/accessibility_reports_spec.rb
+++ b/spec/lib/gitlab/ci/reports/accessibility_reports_spec.rb
@@ -12,7 +12,7 @@ RSpec.describe Gitlab::Ci::Reports::AccessibilityReports do
"type": "error",
"typeCode": 1,
"message": "Anchor element found with a valid href attribute, but no link content has been supplied.",
- "context": %{<a href="/customers/worldline"><svg viewBox="0 0 509 89" xmln...</a>},
+ "context": %(<a href="/customers/worldline"><svg viewBox="0 0 509 89" xmln...</a>),
"selector": "html > body > div:nth-child(9) > div:nth-child(2) > a:nth-child(17)",
"runner": "htmlcs",
"runnerExtras": {}
@@ -22,7 +22,7 @@ RSpec.describe Gitlab::Ci::Reports::AccessibilityReports do
"type": "error",
"typeCode": 1,
"message": "Anchor element found with a valid href attribute, but no link content has been supplied.",
- "context": %{<a href="/customers/equinix"><svg xmlns="http://www.w3.org/...</a>},
+ "context": %(<a href="/customers/equinix"><svg xmlns="http://www.w3.org/...</a>),
"selector": "html > body > div:nth-child(9) > div:nth-child(2) > a:nth-child(18)",
"runner": "htmlcs",
"runnerExtras": {}
diff --git a/spec/lib/gitlab/ci/reports/test_suite_spec.rb b/spec/lib/gitlab/ci/reports/test_suite_spec.rb
index 05f6a8a8cb6..46ab0802200 100644
--- a/spec/lib/gitlab/ci/reports/test_suite_spec.rb
+++ b/spec/lib/gitlab/ci/reports/test_suite_spec.rb
@@ -192,7 +192,7 @@ RSpec.describe Gitlab::Ci::Reports::TestSuite do
end
context 'when there are multiple test cases' do
- let(:status_ordered) { %w(error failed success skipped) }
+ let(:status_ordered) { %w[error failed success skipped] }
before do
test_suite.add_test_case(test_case_success)
diff --git a/spec/lib/gitlab/ci/status/composite_spec.rb b/spec/lib/gitlab/ci/status/composite_spec.rb
index cbf0976c976..e46ad573235 100644
--- a/spec/lib/gitlab/ci/status/composite_spec.rb
+++ b/spec/lib/gitlab/ci/status/composite_spec.rb
@@ -43,28 +43,29 @@ RSpec.describe Gitlab::Ci::Status::Composite, feature_category: :continuous_inte
context 'allow_failure: false' do
where(:build_statuses, :dag, :result, :has_warnings) do
- %i(skipped) | false | 'skipped' | false
- %i(skipped success) | false | 'success' | false
- %i(skipped success) | true | 'skipped' | false
- %i(created) | false | 'created' | false
- %i(preparing) | false | 'preparing' | false
- %i(canceled success skipped) | false | 'canceled' | false
- %i(canceled success skipped) | true | 'skipped' | false
- %i(pending created skipped) | false | 'pending' | false
- %i(pending created skipped success) | false | 'running' | false
- %i(running created skipped success) | false | 'running' | false
- %i(pending created skipped) | true | 'skipped' | false
- %i(pending created skipped success) | true | 'skipped' | false
- %i(running created skipped success) | true | 'skipped' | false
- %i(success waiting_for_resource) | false | 'waiting_for_resource' | false
- %i(success manual) | false | 'manual' | false
- %i(success scheduled) | false | 'scheduled' | false
- %i(created preparing) | false | 'preparing' | false
- %i(created success pending) | false | 'running' | false
- %i(skipped success failed) | false | 'failed' | false
- %i(skipped success failed) | true | 'skipped' | false
- %i(success manual) | true | 'manual' | false
- %i(success failed created) | true | 'running' | false
+ %i[skipped] | false | 'skipped' | false
+ %i[skipped success] | false | 'success' | false
+ %i[skipped success] | true | 'skipped' | false
+ %i[created] | false | 'created' | false
+ %i[preparing] | false | 'preparing' | false
+ %i[canceled success skipped] | false | 'canceled' | false
+ %i[canceled success skipped] | true | 'skipped' | false
+ %i[pending created skipped] | false | 'pending' | false
+ %i[pending created skipped success] | false | 'running' | false
+ %i[running created skipped success] | false | 'running' | false
+ %i[pending created skipped] | true | 'skipped' | false
+ %i[pending created skipped success] | true | 'skipped' | false
+ %i[running created skipped success] | true | 'skipped' | false
+ %i[success waiting_for_resource] | false | 'waiting_for_resource' | false
+ %i[success waiting_for_callback] | false | 'waiting_for_callback' | false
+ %i[success manual] | false | 'manual' | false
+ %i[success scheduled] | false | 'scheduled' | false
+ %i[created preparing] | false | 'preparing' | false
+ %i[created success pending] | false | 'running' | false
+ %i[skipped success failed] | false | 'failed' | false
+ %i[skipped success failed] | true | 'skipped' | false
+ %i[success manual] | true | 'manual' | false
+ %i[success failed created] | true | 'running' | false
end
with_them do
@@ -78,13 +79,13 @@ RSpec.describe Gitlab::Ci::Status::Composite, feature_category: :continuous_inte
context 'allow_failure: true' do
where(:build_statuses, :dag, :result, :has_warnings) do
- %i(manual) | false | 'skipped' | false
- %i(skipped failed) | false | 'success' | true
- %i(skipped failed) | true | 'skipped' | true
- %i(success manual) | true | 'skipped' | false
- %i(success manual) | false | 'success' | false
- %i(created failed) | false | 'created' | true
- %i(preparing manual) | false | 'preparing' | false
+ %i[manual] | false | 'skipped' | false
+ %i[skipped failed] | false | 'success' | true
+ %i[skipped failed] | true | 'skipped' | true
+ %i[success manual] | true | 'skipped' | false
+ %i[success manual] | false | 'success' | false
+ %i[created failed] | false | 'created' | true
+ %i[preparing manual] | false | 'preparing' | false
end
with_them do
diff --git a/spec/lib/gitlab/ci/status/stage/factory_spec.rb b/spec/lib/gitlab/ci/status/stage/factory_spec.rb
index 34e430202c9..98fefea7bdf 100644
--- a/spec/lib/gitlab/ci/status/stage/factory_spec.rb
+++ b/spec/lib/gitlab/ci/status/stage/factory_spec.rb
@@ -22,7 +22,7 @@ RSpec.describe Gitlab::Ci::Status::Stage::Factory, feature_category: :continuous
end
context 'when stage has a core status' do
- (Ci::HasStatus::AVAILABLE_STATUSES - %w(manual skipped scheduled)).each do |core_status|
+ (Ci::HasStatus::AVAILABLE_STATUSES - %w[manual skipped scheduled]).each do |core_status|
context "when core status is #{core_status}" do
let(:stage) { create(:ci_stage, pipeline: pipeline, status: core_status) }
diff --git a/spec/lib/gitlab/ci/status/waiting_for_callback_spec.rb b/spec/lib/gitlab/ci/status/waiting_for_callback_spec.rb
new file mode 100644
index 00000000000..6c833e96137
--- /dev/null
+++ b/spec/lib/gitlab/ci/status/waiting_for_callback_spec.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Ci::Status::WaitingForCallback, feature_category: :deployment_management do
+ subject do
+ described_class.new(double, double)
+ end
+
+ describe '#text' do
+ it { expect(subject.text).to eq 'Waiting' }
+ end
+
+ describe '#label' do
+ it { expect(subject.label).to eq 'waiting for callback' }
+ end
+
+ describe '#icon' do
+ it { expect(subject.icon).to eq 'status_pending' }
+ end
+
+ describe '#favicon' do
+ it { expect(subject.favicon).to eq 'favicon_status_pending' }
+ end
+
+ describe '#group' do
+ it { expect(subject.group).to eq 'waiting-for-callback' }
+ end
+
+ describe '#name' do
+ it { expect(subject.name).to eq 'WAITING_FOR_CALLBACK' }
+ end
+
+ describe '#details_path' do
+ it { expect(subject.details_path).to be_nil }
+ 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 460ecbb05d0..c5125689207 100644
--- a/spec/lib/gitlab/ci/tags/bulk_insert_spec.rb
+++ b/spec/lib/gitlab/ci/tags/bulk_insert_spec.rb
@@ -13,7 +13,7 @@ RSpec.describe Gitlab::Ci::Tags::BulkInsert do
subject(:service) { described_class.new(statuses) }
describe 'gem version' do
- let(:acceptable_version) { '9.0.1' }
+ let(:acceptable_version) { '10.0.0' }
let(:error_message) do
<<~MESSAGE
diff --git a/spec/lib/gitlab/ci/templates/Jobs/deploy_gitlab_ci_yaml_spec.rb b/spec/lib/gitlab/ci/templates/Jobs/deploy_gitlab_ci_yaml_spec.rb
index acb296082b8..dc9999ab9e4 100644
--- a/spec/lib/gitlab/ci/templates/Jobs/deploy_gitlab_ci_yaml_spec.rb
+++ b/spec/lib/gitlab/ci/templates/Jobs/deploy_gitlab_ci_yaml_spec.rb
@@ -58,7 +58,7 @@ RSpec.describe 'Jobs/Deploy.gitlab-ci.yml' do
context 'with no cluster or agent' do
it 'does not create any kubernetes deployment jobs' do
- expect(build_names).to eq %w(placeholder)
+ expect(build_names).to eq %w[placeholder]
end
end
@@ -68,7 +68,7 @@ RSpec.describe 'Jobs/Deploy.gitlab-ci.yml' do
end
it 'does not create any kubernetes deployment jobs' do
- expect(build_names).to eq %w(placeholder)
+ expect(build_names).to eq %w[placeholder]
end
end
@@ -81,7 +81,7 @@ RSpec.describe 'Jobs/Deploy.gitlab-ci.yml' do
it 'when CI_DEPLOY_FREEZE is present' do
create(:ci_variable, project: project, key: 'CI_DEPLOY_FREEZE', value: 'true')
- expect(build_names).to eq %w(placeholder)
+ expect(build_names).to eq %w[placeholder]
end
it 'when CANARY_ENABLED' do
diff --git a/spec/lib/gitlab/ci/templates/Jobs/sast_iac_latest_gitlab_ci_yaml_spec.rb b/spec/lib/gitlab/ci/templates/Jobs/sast_iac_latest_gitlab_ci_yaml_spec.rb
index 2b9213ea921..86bc9259789 100644
--- a/spec/lib/gitlab/ci/templates/Jobs/sast_iac_latest_gitlab_ci_yaml_spec.rb
+++ b/spec/lib/gitlab/ci/templates/Jobs/sast_iac_latest_gitlab_ci_yaml_spec.rb
@@ -44,7 +44,7 @@ RSpec.describe 'Jobs/SAST-IaC.latest.gitlab-ci.yml', feature_category: :continuo
it 'creates a pipeline with the expected jobs' do
expect(pipeline).to be_merge_request_event
expect(pipeline.errors.full_messages).to be_empty
- expect(build_names).to match_array(%w(kics-iac-sast))
+ expect(build_names).to match_array(%w[kics-iac-sast])
end
end
end
diff --git a/spec/lib/gitlab/ci/templates/auto_devops_gitlab_ci_yaml_spec.rb b/spec/lib/gitlab/ci/templates/auto_devops_gitlab_ci_yaml_spec.rb
index 09ca2678de5..7471dc58e44 100644
--- a/spec/lib/gitlab/ci/templates/auto_devops_gitlab_ci_yaml_spec.rb
+++ b/spec/lib/gitlab/ci/templates/auto_devops_gitlab_ci_yaml_spec.rb
@@ -94,7 +94,7 @@ RSpec.describe 'Auto-DevOps.gitlab-ci.yml', feature_category: :auto_devops do
project.repository.create_branch(pipeline_branch, default_branch)
end
- %w(review_ecs review_fargate).each do |job|
+ %w[review_ecs review_fargate].each do |job|
it_behaves_like 'no ECS job when AUTO_DEVOPS_PLATFORM_TARGET is not present' do
let(:job_name) { job }
end
@@ -142,7 +142,7 @@ RSpec.describe 'Auto-DevOps.gitlab-ci.yml', feature_category: :auto_devops do
context 'when the project has no active cluster' do
it 'only creates a build and a test stage' do
- expect(pipeline.stages_names).to eq(%w(build test))
+ expect(pipeline.stages_names).to eq(%w[build test])
end
it_behaves_like 'no Kubernetes deployment job'
@@ -273,25 +273,25 @@ RSpec.describe 'Auto-DevOps.gitlab-ci.yml', feature_category: :auto_devops do
using RSpec::Parameterized::TableSyntax
where(:case_name, :files, :variables, :include_build_names, :not_include_build_names) do
- 'No match' | { 'README.md' => '' } | {} | %w() | %w(build test)
- 'Buildpack' | { 'README.md' => '' } | { 'BUILDPACK_URL' => 'http://example.com' } | %w(build test) | %w()
- 'Explicit set' | { 'README.md' => '' } | { 'AUTO_DEVOPS_EXPLICITLY_ENABLED' => '1' } | %w(build test) | %w()
- 'Explicit unset' | { 'README.md' => '' } | { 'AUTO_DEVOPS_EXPLICITLY_ENABLED' => '0' } | %w() | %w(build test)
- 'DOCKERFILE_PATH' | { 'README.md' => '' } | { 'DOCKERFILE_PATH' => 'Docker.file' } | %w(build test) | %w()
- 'Dockerfile' | { 'Dockerfile' => '' } | {} | %w(build test) | %w()
- 'Clojure' | { 'project.clj' => '' } | {} | %w(build test) | %w()
- 'Go modules' | { 'go.mod' => '' } | {} | %w(build test) | %w()
- 'Go gb' | { 'src/gitlab.com/gopackage.go' => '' } | {} | %w(build test) | %w()
- 'Gradle' | { 'gradlew' => '' } | {} | %w(build test) | %w()
- 'Java' | { 'pom.xml' => '' } | {} | %w(build test) | %w()
- 'Multi-buildpack' | { '.buildpacks' => '' } | {} | %w(build test) | %w()
- 'NodeJS' | { 'package.json' => '' } | {} | %w(build test) | %w()
- 'PHP' | { 'composer.json' => '' } | {} | %w(build test) | %w()
- 'Play' | { 'conf/application.conf' => '' } | {} | %w(build test) | %w()
- 'Python' | { 'Pipfile' => '' } | {} | %w(build test) | %w()
- 'Ruby' | { 'Gemfile' => '' } | {} | %w(build test) | %w()
- 'Scala' | { 'build.sbt' => '' } | {} | %w(build test) | %w()
- 'Static' | { '.static' => '' } | {} | %w(build test) | %w()
+ 'No match' | { 'README.md' => '' } | {} | %w[] | %w[build test]
+ 'Buildpack' | { 'README.md' => '' } | { 'BUILDPACK_URL' => 'http://example.com' } | %w[build test] | %w[]
+ 'Explicit set' | { 'README.md' => '' } | { 'AUTO_DEVOPS_EXPLICITLY_ENABLED' => '1' } | %w[build test] | %w[]
+ 'Explicit unset' | { 'README.md' => '' } | { 'AUTO_DEVOPS_EXPLICITLY_ENABLED' => '0' } | %w[] | %w[build test]
+ 'DOCKERFILE_PATH' | { 'README.md' => '' } | { 'DOCKERFILE_PATH' => 'Docker.file' } | %w[build test] | %w[]
+ 'Dockerfile' | { 'Dockerfile' => '' } | {} | %w[build test] | %w[]
+ 'Clojure' | { 'project.clj' => '' } | {} | %w[build test] | %w[]
+ 'Go modules' | { 'go.mod' => '' } | {} | %w[build test] | %w[]
+ 'Go gb' | { 'src/gitlab.com/gopackage.go' => '' } | {} | %w[build test] | %w[]
+ 'Gradle' | { 'gradlew' => '' } | {} | %w[build test] | %w[]
+ 'Java' | { 'pom.xml' => '' } | {} | %w[build test] | %w[]
+ 'Multi-buildpack' | { '.buildpacks' => '' } | {} | %w[build test] | %w[]
+ 'NodeJS' | { 'package.json' => '' } | {} | %w[build test] | %w[]
+ 'PHP' | { 'composer.json' => '' } | {} | %w[build test] | %w[]
+ 'Play' | { 'conf/application.conf' => '' } | {} | %w[build test] | %w[]
+ 'Python' | { 'Pipfile' => '' } | {} | %w[build test] | %w[]
+ 'Ruby' | { 'Gemfile' => '' } | {} | %w[build test] | %w[]
+ 'Scala' | { 'build.sbt' => '' } | {} | %w[build test] | %w[]
+ 'Static' | { '.static' => '' } | {} | %w[build test] | %w[]
end
with_them do
diff --git a/spec/lib/gitlab/ci/variables/collection/item_spec.rb b/spec/lib/gitlab/ci/variables/collection/item_spec.rb
index d96c8f1bd0c..aa612899f4b 100644
--- a/spec/lib/gitlab/ci/variables/collection/item_spec.rb
+++ b/spec/lib/gitlab/ci/variables/collection/item_spec.rb
@@ -123,11 +123,11 @@ RSpec.describe Gitlab::Ci::Variables::Collection::Item, feature_category: :secre
},
"simple variable reference": {
variable: { key: 'VAR', value: 'something_$VAR2' },
- expected_depends_on: %w(VAR2)
+ expected_depends_on: %w[VAR2]
},
"complex expansion": {
variable: { key: 'VAR', value: 'something_${VAR2}_$VAR3' },
- expected_depends_on: %w(VAR2 VAR3)
+ expected_depends_on: %w[VAR2 VAR3]
},
"complex expansion in raw variable": {
variable: { key: 'VAR', value: 'something_${VAR2}_$VAR3', raw: true },
@@ -135,7 +135,7 @@ RSpec.describe Gitlab::Ci::Variables::Collection::Item, feature_category: :secre
},
"complex expansions for Windows": {
variable: { key: 'variable3', value: 'key%variable%%variable2%' },
- expected_depends_on: %w(variable variable2)
+ expected_depends_on: %w[variable variable2]
}
}
end
@@ -282,7 +282,7 @@ RSpec.describe Gitlab::Ci::Variables::Collection::Item, feature_category: :secre
it '#depends_on contains names of dependencies' do
runner_variable = described_class.new(key: 'CI_VAR', value: '${CI_VAR_2}-123-$CI_VAR_3')
- expect(runner_variable.depends_on).to eq(%w(CI_VAR_2 CI_VAR_3))
+ expect(runner_variable.depends_on).to eq(%w[CI_VAR_2 CI_VAR_3])
end
end
diff --git a/spec/lib/gitlab/ci/yaml_processor/dag_spec.rb b/spec/lib/gitlab/ci/yaml_processor/dag_spec.rb
index 082febacbd7..496d89403d5 100644
--- a/spec/lib/gitlab/ci/yaml_processor/dag_spec.rb
+++ b/spec/lib/gitlab/ci/yaml_processor/dag_spec.rb
@@ -3,38 +3,48 @@
require 'fast_spec_helper'
require 'tsort'
-RSpec.describe Gitlab::Ci::YamlProcessor::Dag do
+RSpec.describe Gitlab::Ci::YamlProcessor::Dag, feature_category: :pipeline_composition do
let(:nodes) { {} }
subject(:result) { described_class.new(nodes).tsort }
context 'when it is a regular pipeline' do
let(:nodes) do
- { 'job_c' => %w(job_b job_d), 'job_d' => %w(job_a), 'job_b' => %w(job_a), 'job_a' => %w() }
+ { 'job_c' => %w[job_b job_d], 'job_d' => %w[job_a], 'job_b' => %w[job_a], 'job_a' => %w[] }
end
it 'returns ordered jobs' do
- expect(result).to eq(%w(job_a job_b job_d job_c))
+ expect(result).to eq(%w[job_a job_b job_d job_c])
end
end
context 'when there is a circular dependency' do
let(:nodes) do
- { 'job_a' => %w(job_c), 'job_b' => %w(job_a), 'job_c' => %w(job_b) }
+ { 'job_a' => %w[job_c], 'job_b' => %w[job_a], 'job_c' => %w[job_b] }
end
- it 'raises TSort::Cyclic' do
+ it 'raises TSort::Cyclic error' do
expect { result }.to raise_error(TSort::Cyclic, /topological sort failed/)
end
+
+ context 'when a job has a self-dependency' do
+ let(:nodes) do
+ { 'job_a' => %w[job_a] }
+ end
+
+ it 'raises TSort::Cyclic error' do
+ expect { result }.to raise_error(TSort::Cyclic, "self-dependency: job_a")
+ end
+ end
end
context 'when there are some missing jobs' do
let(:nodes) do
- { 'job_a' => %w(job_d job_f), 'job_b' => %w(job_a job_c job_e) }
+ { 'job_a' => %w[job_d job_f], 'job_b' => %w[job_a job_c job_e] }
end
it 'ignores the missing ones and returns in a valid order' do
- expect(result).to eq(%w(job_d job_f job_a job_c job_e job_b))
+ expect(result).to eq(%w[job_d job_f job_a job_c job_e job_b])
end
end
end
diff --git a/spec/lib/gitlab/ci/yaml_processor_spec.rb b/spec/lib/gitlab/ci/yaml_processor_spec.rb
index 81bc8c7ab9a..f01c1c7d053 100644
--- a/spec/lib/gitlab/ci/yaml_processor_spec.rb
+++ b/spec/lib/gitlab/ci/yaml_processor_spec.rb
@@ -1510,7 +1510,7 @@ module Gitlab
it 'correctly extends rspec job' do
expect(config_processor.builds).to be_one
- expect(subject.dig(:options, :script)).to eq %w(test)
+ expect(subject.dig(:options, :script)).to eq %w[test]
expect(subject.dig(:options, :image, :name)).to eq 'ruby:alpine'
end
end
@@ -1595,7 +1595,7 @@ module Gitlab
it 'correctly extends rspec job' do
expect(config_processor.builds).to be_one
expect(subject.dig(:options, :before_script)).to eq ["bundle install"]
- expect(subject.dig(:options, :script)).to eq %w(rspec)
+ expect(subject.dig(:options, :script)).to eq %w[rspec]
expect(subject.dig(:options, :image, :name)).to eq 'image:test'
expect(subject.dig(:when)).to eq 'always'
end
@@ -2386,7 +2386,7 @@ module Gitlab
end
context 'dependencies to builds' do
- let(:dependencies) { %w(build1 build2) }
+ let(:dependencies) { %w[build1 build2] }
it { is_expected.to be_valid }
end
@@ -2457,7 +2457,7 @@ module Gitlab
end
context 'needs a job from the same stage' do
- let(:needs) { %w(test2) }
+ let(:needs) { %w[test2] }
it 'creates jobs with valid specifications' do
expect(subject.builds.size).to eq(7)
@@ -2494,7 +2494,7 @@ module Gitlab
end
context 'needs two builds' do
- let(:needs) { %w(build1 build2) }
+ let(:needs) { %w[build1 build2] }
it "does create jobs with valid specification" do
expect(subject.builds.size).to eq(7)
@@ -2578,7 +2578,7 @@ module Gitlab
end
context 'needs parallel job' do
- let(:needs) { %w(parallel) }
+ let(:needs) { %w[parallel] }
it "does create jobs with valid specification" do
expect(subject.builds.size).to eq(7)
@@ -2707,7 +2707,7 @@ module Gitlab
context 'duplicate needs' do
context 'when needs are specified in an array' do
- let(:needs) { %w(build1 build1) }
+ let(:needs) { %w[build1 build1] }
it_behaves_like 'returns errors', 'test1 has the following needs duplicated: build1.'
end
@@ -2736,8 +2736,8 @@ module Gitlab
end
context 'needs and dependencies that are mismatching' do
- let(:needs) { %w(build1) }
- let(:dependencies) { %w(build2) }
+ let(:needs) { %w[build1] }
+ let(:dependencies) { %w[build2] }
it_behaves_like 'returns errors', 'jobs:test1 dependencies the build2 should be part of needs'
end
@@ -2750,13 +2750,13 @@ module Gitlab
]
end
- let(:dependencies) { %w(build3) }
+ let(:dependencies) { %w[build3] }
it_behaves_like 'returns errors', 'jobs:test1 dependencies the build3 should be part of needs'
end
context 'needs with an array type and dependency with a string type' do
- let(:needs) { %w(build1) }
+ let(:needs) { %w[build1] }
let(:dependencies) { 'deploy' }
it_behaves_like 'returns errors', 'jobs:test1 dependencies should be an array of strings'
@@ -2764,7 +2764,7 @@ module Gitlab
context 'needs with a string type and dependency with an array type' do
let(:needs) { 'build1' }
- let(:dependencies) { %w(deploy) }
+ let(:dependencies) { %w[deploy] }
it_behaves_like 'returns errors', 'jobs:test1:needs config can only be a hash or an array'
end
@@ -3252,7 +3252,7 @@ module Gitlab
end
context 'returns errors if job stage is not a defined stage' do
- let(:config) { YAML.dump({ stages: %w(build test), rspec: { script: "test", stage: "acceptance" } }) }
+ let(:config) { YAML.dump({ stages: %w[build test], rspec: { script: "test", stage: "acceptance" } }) }
it_behaves_like 'returns errors', 'rspec job: chosen stage does not exist; available stages are .pre, build, test, .post'
end
@@ -3288,37 +3288,37 @@ module Gitlab
end
context 'returns errors if job artifacts:name is not an a string' do
- let(:config) { YAML.dump({ stages: %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({ stages: %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 one of: on_success, on_failure, always'
end
context 'returns errors if job artifacts:expire_in is not an a string' do
- let(:config) { YAML.dump({ stages: %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({ stages: %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({ stages: %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({ stages: %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
@@ -3342,49 +3342,49 @@ module Gitlab
end
context 'returns errors if job cache:key is not an a string' do
- let(:config) { YAML.dump({ stages: %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({ stages: %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({ stages: %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({ stages: %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({ stages: %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({ stages: %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({ stages: %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({ stages: %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
@@ -3433,7 +3433,24 @@ module Gitlab
YAML
end
- it_behaves_like 'returns errors', 'The pipeline has circular dependencies'
+ it_behaves_like 'returns errors', 'The pipeline has circular dependencies: topological sort failed: ["job_a", "job_c", "job_b"]'
+
+ context 'when a job has a self-dependency' do
+ let(:config) do
+ <<~YAML
+ job_0:
+ stage: test
+ script: build
+
+ job:
+ stage: test
+ script: build
+ needs: [job_0, job]
+ YAML
+ end
+
+ it_behaves_like 'returns errors', 'The pipeline has circular dependencies: self-dependency: job'
+ end
end
end
@@ -3668,6 +3685,70 @@ module Gitlab
it { is_expected.to be_valid }
end
end
+
+ context 'for pages jobs', feature_category: :pages do
+ context 'on publish option' do
+ context 'when not in a pages job' do
+ let(:config) do
+ <<-EOYML
+ not-pages:
+ script: echo
+ publish: 'foo'
+ EOYML
+ end
+
+ it_behaves_like 'returns errors', 'jobs:not-pages publish can only be used within a `pages` job'
+ end
+
+ context 'when in a pages job' do
+ let(:config) do
+ <<-EOYML
+ pages:
+ script: echo
+ publish: 'foo'
+ EOYML
+ end
+
+ it { is_expected.to be_valid }
+
+ it 'sets the publish configuration' do
+ expect(subject.builds.first[:options][:publish]).to eq('foo')
+ end
+ end
+ end
+
+ context 'on pages option' do
+ context 'when not in a pages job' do
+ let(:config) do
+ <<-EOYML
+ not-pages:
+ script: echo
+ pages:
+ path_prefix: 'foo'
+ EOYML
+ end
+
+ it_behaves_like 'returns errors', 'jobs:not-pages pages can only be used within a `pages` job'
+ end
+
+ context 'when in a pages job' do
+ let(:config) do
+ <<-EOYML
+ pages:
+ script: echo
+ pages:
+ path_prefix: 'foo'
+ EOYML
+ end
+
+ it { is_expected.to be_valid }
+
+ it 'sets the pages configuration' do
+ expect(subject.builds.first[:options][:pages]).to eq(path_prefix: 'foo')
+ end
+ end
+ end
+ end
end
end
end
diff --git a/spec/lib/gitlab/composer/version_index_spec.rb b/spec/lib/gitlab/composer/version_index_spec.rb
index 63efa8cae95..c5bc6dc0195 100644
--- a/spec/lib/gitlab/composer/version_index_spec.rb
+++ b/spec/lib/gitlab/composer/version_index_spec.rb
@@ -83,32 +83,6 @@ RSpec.describe Gitlab::Composer::VersionIndex, feature_category: :package_regist
it_behaves_like 'returns the packages json'
end
-
- context 'with composer_use_ssh_source_urls disabled' do
- before do
- stub_feature_flags(composer_use_ssh_source_urls: false)
- end
-
- context 'with a public project' do
- it_behaves_like 'returns the packages json'
- end
-
- context 'with an internal project' do
- before do
- project.update!(visibility: Gitlab::VisibilityLevel::INTERNAL)
- end
-
- it_behaves_like 'returns the packages json'
- end
-
- context 'with a private project' do
- before do
- project.update!(visibility: Gitlab::VisibilityLevel::PRIVATE)
- end
-
- it_behaves_like 'returns the packages json'
- end
- end
end
describe '#sha' do
diff --git a/spec/lib/gitlab/config/entry/factory_spec.rb b/spec/lib/gitlab/config/entry/factory_spec.rb
index be4dfd31651..bbbba0cf7cd 100644
--- a/spec/lib/gitlab/config/entry/factory_spec.rb
+++ b/spec/lib/gitlab/config/entry/factory_spec.rb
@@ -21,16 +21,16 @@ RSpec.describe Gitlab::Config::Entry::Factory do
context 'when setting a concrete value' do
it 'creates entry with valid value' do
entry = factory
- .value(%w(ls pwd))
+ .value(%w[ls pwd])
.create!
- expect(entry.value).to eq %w(ls pwd)
+ expect(entry.value).to eq %w[ls pwd]
end
context 'when setting description' do
before do
factory
- .value(%w(ls pwd))
+ .value(%w[ls pwd])
.with(description: 'test description')
end
@@ -41,7 +41,7 @@ RSpec.describe Gitlab::Config::Entry::Factory do
it 'creates entry with description' do
entry = factory.create!
- expect(entry.value).to eq %w(ls pwd)
+ expect(entry.value).to eq %w[ls pwd]
expect(entry.description).to eq 'test description'
end
end
@@ -49,7 +49,7 @@ RSpec.describe Gitlab::Config::Entry::Factory do
context 'when setting inherit' do
before do
factory
- .value(%w(ls pwd))
+ .value(%w[ls pwd])
.with(inherit: true)
end
@@ -61,7 +61,7 @@ RSpec.describe Gitlab::Config::Entry::Factory do
context 'when setting key' do
it 'creates entry with custom key' do
entry = factory
- .value(%w(ls pwd))
+ .value(%w[ls pwd])
.with(key: 'test key')
.create!
diff --git a/spec/lib/gitlab/config/entry/validators_spec.rb b/spec/lib/gitlab/config/entry/validators_spec.rb
index 6fa9f9d0767..e13c09f97ca 100644
--- a/spec/lib/gitlab/config/entry/validators_spec.rb
+++ b/spec/lib/gitlab/config/entry/validators_spec.rb
@@ -35,7 +35,7 @@ RSpec.describe Gitlab::Config::Entry::Validators, feature_category: :pipeline_co
expect(instance.valid?).to be(valid_result)
unless valid_result
- expect(instance.errors.messages_for(:config)).to include /please use only one of the following keys: foo, bar/
+ expect(instance.errors.messages_for(:config)).to include(/these keys cannot be used together: foo, bar/)
end
end
end
diff --git a/spec/lib/gitlab/config_checker/puma_rugged_checker_spec.rb b/spec/lib/gitlab/config_checker/puma_rugged_checker_spec.rb
deleted file mode 100644
index afee3c5536e..00000000000
--- a/spec/lib/gitlab/config_checker/puma_rugged_checker_spec.rb
+++ /dev/null
@@ -1,65 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Gitlab::ConfigChecker::PumaRuggedChecker do
- describe '#check' do
- subject { described_class.check }
-
- context 'application is not puma' do
- before do
- allow(Gitlab::Runtime).to receive(:puma?).and_return(false)
- end
-
- it { is_expected.to be_empty }
- end
-
- context 'application is puma' do
- let(:notice_multi_threaded_puma_with_rugged) do
- {
- type: 'warning',
- message: 'Puma is running with a thread count above 1 and the Rugged '\
- 'service is enabled. This may decrease performance in some environments. '\
- 'See our <a href="https://docs.gitlab.com/ee/administration/operations/puma.html#performance-caveat-when-using-puma-with-rugged">documentation</a> '\
- 'for details of this issue.'
- }
- end
-
- before do
- allow(Gitlab::Runtime).to receive(:puma?).and_return(true)
- allow(described_class).to receive(:running_puma_with_multiple_threads?).and_return(multithreaded_puma)
- allow(described_class).to receive(:rugged_enabled_through_feature_flag?).and_return(rugged_enabled)
- end
-
- context 'not multithreaded_puma and rugged API enabled' do
- let(:multithreaded_puma) { false }
- let(:rugged_enabled) { true }
-
- it { is_expected.to be_empty }
- end
-
- context 'not multithreaded_puma and rugged API is not enabled' do
- let(:multithreaded_puma) { false }
- let(:rugged_enabled) { false }
-
- it { is_expected.to be_empty }
- end
-
- context 'multithreaded_puma and rugged API is not enabled' do
- let(:multithreaded_puma) { true }
- let(:rugged_enabled) { false }
-
- it { is_expected.to be_empty }
- end
-
- context 'multithreaded_puma and rugged API is enabled' do
- let(:multithreaded_puma) { true }
- let(:rugged_enabled) { true }
-
- it 'report multi_threaded_puma_with_rugged notices' do
- is_expected.to contain_exactly(notice_multi_threaded_puma_with_rugged)
- end
- end
- end
- end
-end
diff --git a/spec/lib/gitlab/conflict/file_spec.rb b/spec/lib/gitlab/conflict/file_spec.rb
index 6ea8e6c6706..49252a6537c 100644
--- a/spec/lib/gitlab/conflict/file_spec.rb
+++ b/spec/lib/gitlab/conflict/file_spec.rb
@@ -58,7 +58,7 @@ RSpec.describe Gitlab::Conflict::File do
it 'returns a file containing only the chosen parts of the resolved sections' do
expect(resolved_lines.chunk { |line| line.type || 'both' }.map(&:first))
- .to eq(%w(both new both old both new both))
+ .to eq(%w[both new both old both new both])
end
end
@@ -193,7 +193,7 @@ RSpec.describe Gitlab::Conflict::File do
it 'sets conflict to true for sections with only changed lines' do
conflict_file.sections.select { |section| section[:conflict] }.each do |section|
section[:lines].each do |line|
- expect(line.type).to be_in(%w(new old))
+ expect(line.type).to be_in(%w[new old])
end
end
end
diff --git a/spec/lib/gitlab/data_builder/build_spec.rb b/spec/lib/gitlab/data_builder/build_spec.rb
index 66890315ee8..7afd16f53e5 100644
--- a/spec/lib/gitlab/data_builder/build_spec.rb
+++ b/spec/lib/gitlab/data_builder/build_spec.rb
@@ -49,7 +49,7 @@ RSpec.describe Gitlab::DataBuilder::Build, feature_category: :integrations do
it { expect(data[:commit][:id]).to eq(ci_build.pipeline.id) }
it { expect(data[:runner][:id]).to eq(ci_build.runner.id) }
- it { expect(data[:runner][:tags]).to match_array(%w(tag1 tag2)) }
+ it { expect(data[:runner][:tags]).to match_array(%w[tag1 tag2]) }
it { expect(data[:runner][:description]).to eq(ci_build.runner.description) }
it { expect(data[:runner][:runner_type]).to eq(ci_build.runner.runner_type) }
it { expect(data[:runner][:is_shared]).to eq(ci_build.runner.instance_type?) }
diff --git a/spec/lib/gitlab/data_builder/pipeline_spec.rb b/spec/lib/gitlab/data_builder/pipeline_spec.rb
index 351872ffbc5..ad7cd2dc736 100644
--- a/spec/lib/gitlab/data_builder/pipeline_spec.rb
+++ b/spec/lib/gitlab/data_builder/pipeline_spec.rb
@@ -66,7 +66,7 @@ RSpec.describe Gitlab::DataBuilder::Pipeline, feature_category: :continuous_inte
end
context 'build with runner' do
- let_it_be(:tag_names) { %w(tag-1 tag-2) }
+ let_it_be(:tag_names) { %w[tag-1 tag-2] }
let_it_be(:ci_runner) { create(:ci_runner, tag_list: tag_names.map { |n| ActsAsTaggableOn::Tag.create!(name: n) }) }
let_it_be(:build) { create(:ci_build, pipeline: pipeline, runner: ci_runner) }
diff --git a/spec/lib/gitlab/data_builder/push_spec.rb b/spec/lib/gitlab/data_builder/push_spec.rb
index a3dd4e49e83..02dc596c5eb 100644
--- a/spec/lib/gitlab/data_builder/push_spec.rb
+++ b/spec/lib/gitlab/data_builder/push_spec.rb
@@ -26,7 +26,7 @@ RSpec.describe Gitlab::DataBuilder::Push do
it 'returns commit hook data' do
expect(subject[:project]).to eq(project.hook_attrs)
- expect(subject[:commits].first.keys).to include(*%i(added removed modified))
+ expect(subject[:commits].first.keys).to include(*%i[added removed modified])
end
end
@@ -35,7 +35,7 @@ RSpec.describe Gitlab::DataBuilder::Push do
it 'returns commit hook data without include deltas' do
expect(subject[:project]).to eq(project.hook_attrs)
- expect(subject[:commits].first.keys).not_to include(*%i(added removed modified))
+ expect(subject[:commits].first.keys).not_to include(*%i[added removed modified])
end
end
end
diff --git a/spec/lib/gitlab/database/background_migration/batched_job_spec.rb b/spec/lib/gitlab/database/background_migration/batched_job_spec.rb
index d9b81a2be30..e1d1674d05c 100644
--- a/spec/lib/gitlab/database/background_migration/batched_job_spec.rb
+++ b/spec/lib/gitlab/database/background_migration/batched_job_spec.rb
@@ -25,7 +25,7 @@ RSpec.describe Gitlab::Database::BackgroundMigration::BatchedJob, type: :model d
describe 'state machine' do
let_it_be(:job) { create(:batched_background_migration_job, :failed) }
- it { expect(described_class.state_machine.states.map(&:name)).to eql(%i(pending running failed succeeded)) }
+ it { expect(described_class.state_machine.states.map(&:name)).to eql(%i[pending running failed succeeded]) }
context 'when a job is running' do
it 'logs the transition' do
diff --git a/spec/lib/gitlab/database/background_migration/batched_job_transition_log_spec.rb b/spec/lib/gitlab/database/background_migration/batched_job_transition_log_spec.rb
index 59f4f40c0ef..7cf7be8ffc2 100644
--- a/spec/lib/gitlab/database/background_migration/batched_job_transition_log_spec.rb
+++ b/spec/lib/gitlab/database/background_migration/batched_job_transition_log_spec.rb
@@ -15,7 +15,7 @@ RSpec.describe Gitlab::Database::BackgroundMigration::BatchedJobTransitionLog, t
it { is_expected.to validate_presence_of(:batched_job) }
it { is_expected.to validate_length_of(:exception_class).is_at_most(100) }
it { is_expected.to validate_length_of(:exception_message).is_at_most(1000) }
- it { is_expected.to define_enum_for(:previous_status).with_values(%i(pending running failed succeeded)).with_prefix }
- it { is_expected.to define_enum_for(:next_status).with_values(%i(pending running failed succeeded)).with_prefix }
+ it { is_expected.to define_enum_for(:previous_status).with_values(%i[pending running failed succeeded]).with_prefix }
+ it { is_expected.to define_enum_for(:next_status).with_values(%i[pending running failed succeeded]).with_prefix }
end
end
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 213dee0d19d..f70b38377d8 100644
--- a/spec/lib/gitlab/database/background_migration/batched_migration_spec.rb
+++ b/spec/lib/gitlab/database/background_migration/batched_migration_spec.rb
@@ -422,11 +422,12 @@ RSpec.describe Gitlab::Database::BackgroundMigration::BatchedMigration, type: :m
describe '#create_batched_job!' do
let(:batched_migration) do
- create(:batched_background_migration,
- batch_size: 999,
- sub_batch_size: 99,
- pause_ms: 250
- )
+ create(
+ :batched_background_migration,
+ batch_size: 999,
+ sub_batch_size: 99,
+ pause_ms: 250
+ )
end
it 'creates a batched_job with the correct batch configuration' do
diff --git a/spec/lib/gitlab/database/background_migration/batched_migration_wrapper_spec.rb b/spec/lib/gitlab/database/background_migration/batched_migration_wrapper_spec.rb
index 8d74d16f4e5..bbcb65109ce 100644
--- a/spec/lib/gitlab/database/background_migration/batched_migration_wrapper_spec.rb
+++ b/spec/lib/gitlab/database/background_migration/batched_migration_wrapper_spec.rb
@@ -32,17 +32,17 @@ RSpec.describe Gitlab::Database::BackgroundMigration::BatchedMigrationWrapper, '
end
it 'runs the migration job' do
- expect(job_class).to receive(:new)
- .with(start_id: 1,
- end_id: 10,
- batch_table: 'events',
- batch_column: 'id',
- sub_batch_size: 1,
- pause_ms: pause_ms,
- job_arguments: active_migration.job_arguments,
- connection: connection,
- sub_batch_exception: sub_batch_exception)
- .and_return(job_instance)
+ expect(job_class).to receive(:new).with(
+ start_id: 1,
+ end_id: 10,
+ batch_table: 'events',
+ batch_column: 'id',
+ sub_batch_size: 1,
+ pause_ms: pause_ms,
+ job_arguments: active_migration.job_arguments,
+ connection: connection,
+ sub_batch_exception: sub_batch_exception
+ ).and_return(job_instance)
expect(job_instance).to receive(:perform).with(no_args)
diff --git a/spec/lib/gitlab/database/background_migration/prometheus_metrics_spec.rb b/spec/lib/gitlab/database/background_migration/prometheus_metrics_spec.rb
index 1f256de35ec..8f380a8229c 100644
--- a/spec/lib/gitlab/database/background_migration/prometheus_metrics_spec.rb
+++ b/spec/lib/gitlab/database/background_migration/prometheus_metrics_spec.rb
@@ -5,11 +5,14 @@ require 'spec_helper'
RSpec.describe Gitlab::Database::BackgroundMigration::PrometheusMetrics, :prometheus do
describe '#track' do
let(:job_record) do
- build(:batched_background_migration_job, :succeeded,
- started_at: Time.current - 2.minutes,
- finished_at: Time.current - 1.minute,
- updated_at: Time.current,
- metrics: { 'timings' => { 'update_all' => [0.05, 0.2, 0.4, 0.9, 4] } })
+ build(
+ :batched_background_migration_job,
+ :succeeded,
+ started_at: Time.current - 2.minutes,
+ finished_at: Time.current - 1.minute,
+ updated_at: Time.current,
+ metrics: { 'timings' => { 'update_all' => [0.05, 0.2, 0.4, 0.9, 4] } }
+ )
end
let(:labels) { job_record.batched_migration.prometheus_labels }
diff --git a/spec/lib/gitlab/database/bulk_update_spec.rb b/spec/lib/gitlab/database/bulk_update_spec.rb
index fa519cffd6b..2f0859dba74 100644
--- a/spec/lib/gitlab/database/bulk_update_spec.rb
+++ b/spec/lib/gitlab/database/bulk_update_spec.rb
@@ -92,7 +92,7 @@ RSpec.describe Gitlab::Database::BulkUpdate do
end
context 'validates prepared_statements support', :reestablished_active_record_base,
- :suppress_gitlab_schemas_validate_connection do
+ :suppress_gitlab_schemas_validate_connection do
using RSpec::Parameterized::TableSyntax
where(:prepared_statements) do
diff --git a/spec/lib/gitlab/database/dictionary_spec.rb b/spec/lib/gitlab/database/dictionary_spec.rb
new file mode 100644
index 00000000000..6d2de41468b
--- /dev/null
+++ b/spec/lib/gitlab/database/dictionary_spec.rb
@@ -0,0 +1,84 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Database::Dictionary, feature_category: :database do
+ subject(:database_dictionary) { described_class.new(file_path) }
+
+ context 'for a table' do
+ let(:file_path) { 'db/docs/application_settings.yml' }
+
+ describe '#name_and_schema' do
+ it 'returns the name of the table and its gitlab schema' do
+ expect(database_dictionary.name_and_schema).to match_array(['application_settings', :gitlab_main_clusterwide])
+ end
+ end
+
+ describe '#table_name' do
+ it 'returns the name of the table' do
+ expect(database_dictionary.table_name).to eq('application_settings')
+ end
+ end
+
+ describe '#view_name' do
+ it 'returns nil' do
+ expect(database_dictionary.view_name).to be_nil
+ end
+ end
+
+ describe '#milestone' do
+ it 'returns the milestone in which the table was introduced' do
+ expect(database_dictionary.milestone).to eq('7.7')
+ end
+ end
+
+ describe '#gitlab_schema' do
+ it 'returns the gitlab_schema of the table' do
+ expect(database_dictionary.table_name).to eq('application_settings')
+ end
+ end
+
+ describe '#schema?' do
+ it 'checks if the given schema matches the schema of the table' do
+ expect(database_dictionary.schema?('gitlab_main')).to eq(false)
+ expect(database_dictionary.schema?('gitlab_main_clusterwide')).to eq(true)
+ end
+ end
+
+ describe '#key_name' do
+ it 'returns the value of the name of the table' do
+ expect(database_dictionary.key_name).to eq('application_settings')
+ end
+ end
+
+ describe '#validate!' do
+ it 'raises an error if the gitlab_schema is empty' do
+ allow(database_dictionary).to receive(:gitlab_schema).and_return(nil)
+
+ expect { database_dictionary.validate! }.to raise_error(Gitlab::Database::GitlabSchema::UnknownSchemaError)
+ end
+ end
+ end
+
+ context 'for a view' do
+ let(:file_path) { 'db/docs/views/postgres_constraints.yml' }
+
+ describe '#table_name' do
+ it 'returns nil' do
+ expect(database_dictionary.table_name).to be_nil
+ end
+ end
+
+ describe '#view_name' do
+ it 'returns the name of the view' do
+ expect(database_dictionary.view_name).to eq('postgres_constraints')
+ end
+ end
+
+ describe '#key_name' do
+ it 'returns the value of the name of the view' do
+ expect(database_dictionary.key_name).to eq('postgres_constraints')
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/database/dynamic_model_helpers_spec.rb b/spec/lib/gitlab/database/dynamic_model_helpers_spec.rb
index fe423b3639b..7ab50d47408 100644
--- a/spec/lib/gitlab/database/dynamic_model_helpers_spec.rb
+++ b/spec/lib/gitlab/database/dynamic_model_helpers_spec.rb
@@ -2,28 +2,83 @@
require 'spec_helper'
-RSpec.describe Gitlab::Database::DynamicModelHelpers do
+RSpec.describe Gitlab::Database::DynamicModelHelpers, feature_category: :database do
let(:including_class) { Class.new.include(described_class) }
let(:table_name) { Project.table_name }
let(:connection) { Project.connection }
describe '#define_batchable_model' do
- subject { including_class.new.define_batchable_model(table_name, connection: connection) }
+ subject(:model) { including_class.new.define_batchable_model(table_name, connection: connection) }
it 'is an ActiveRecord model' do
- expect(subject.ancestors).to include(ActiveRecord::Base)
+ expect(model.ancestors).to include(ActiveRecord::Base)
end
it 'includes EachBatch' do
- expect(subject.included_modules).to include(EachBatch)
+ expect(model.included_modules).to include(EachBatch)
end
it 'has the correct table name' do
- expect(subject.table_name).to eq(table_name)
+ expect(model.table_name).to eq(table_name)
end
it 'has the inheritance type column disable' do
- expect(subject.inheritance_column).to eq('_type_disabled')
+ expect(model.inheritance_column).to eq('_type_disabled')
+ end
+
+ context 'for primary key' do
+ subject(:model) do
+ including_class.new.define_batchable_model(table_name, connection: connection, primary_key: primary_key)
+ end
+
+ context 'when table primary key is a single column' do
+ let(:primary_key) { nil }
+
+ context 'when primary key is nil' do
+ it 'does not change the primary key from :id' do
+ expect(model.primary_key).to eq('id')
+ end
+ end
+
+ context 'when primary key is not nil' do
+ let(:primary_key) { 'other_column' }
+
+ it 'does not change the primary key from :id' do
+ expect(model.primary_key).to eq('id')
+ end
+ end
+ end
+
+ context 'when table has composite primary key' do
+ let(:primary_key) { nil }
+ let(:table_name) { :_test_composite_primary_key }
+
+ before do
+ connection.execute(<<~SQL)
+ DROP TABLE IF EXISTS #{table_name};
+
+ CREATE TABLE #{table_name} (
+ id integer NOT NULL,
+ partition_id integer NOT NULL,
+ PRIMARY KEY (id, partition_id)
+ );
+ SQL
+ end
+
+ context 'when primary key is nil' do
+ it 'does not change the primary key from nil' do
+ expect(model.primary_key).to be_nil
+ end
+ end
+
+ context 'when primary key is not nil' do
+ let(:primary_key) { 'id' }
+
+ it 'changes the primary key' do
+ expect(model.primary_key).to eq('id')
+ end
+ end
+ end
end
end
diff --git a/spec/lib/gitlab/database/gitlab_schema_spec.rb b/spec/lib/gitlab/database/gitlab_schema_spec.rb
index a6de695c345..a47e53c18a5 100644
--- a/spec/lib/gitlab/database/gitlab_schema_spec.rb
+++ b/spec/lib/gitlab/database/gitlab_schema_spec.rb
@@ -95,10 +95,10 @@ RSpec.describe Gitlab::Database::GitlabSchema, feature_category: :database do
# ignore gitlab_internal due to `ar_internal_metadata`, `schema_migrations`
table_and_view_names = table_and_view_names
- .reject { |_, gitlab_schema| gitlab_schema == :gitlab_internal }
+ .reject { |database_dictionary| database_dictionary.schema?('gitlab_internal') }
duplicated_tables = table_and_view_names
- .group_by(&:first)
+ .group_by(&:key_name)
.select { |_, schemas| schemas.count > 1 }
.keys
diff --git a/spec/lib/gitlab/database/loose_foreign_keys_spec.rb b/spec/lib/gitlab/database/loose_foreign_keys_spec.rb
index 552df64096a..1824a50cb28 100644
--- a/spec/lib/gitlab/database/loose_foreign_keys_spec.rb
+++ b/spec/lib/gitlab/database/loose_foreign_keys_spec.rb
@@ -8,14 +8,14 @@ RSpec.describe Gitlab::Database::LooseForeignKeys do
it 'all definitions have assigned a known gitlab_schema and on_delete' do
is_expected.to all(have_attributes(
- options: a_hash_including(
- column: be_a(String),
- gitlab_schema: be_in(Gitlab::Database.schemas_to_base_models.symbolize_keys.keys),
- on_delete: be_in([:async_delete, :async_nullify])
- ),
- from_table: be_a(String),
- to_table: be_a(String)
- ))
+ options: a_hash_including(
+ column: be_a(String),
+ gitlab_schema: be_in(Gitlab::Database.schemas_to_base_models.symbolize_keys.keys),
+ on_delete: be_in([:async_delete, :async_nullify])
+ ),
+ from_table: be_a(String),
+ to_table: be_a(String)
+ ))
end
context 'ensure keys are sorted' do
diff --git a/spec/lib/gitlab/database/migration_helpers/cascading_namespace_settings_spec.rb b/spec/lib/gitlab/database/migration_helpers/cascading_namespace_settings_spec.rb
index e11ffe53c61..fb3da38a7be 100644
--- a/spec/lib/gitlab/database/migration_helpers/cascading_namespace_settings_spec.rb
+++ b/spec/lib/gitlab/database/migration_helpers/cascading_namespace_settings_spec.rb
@@ -27,7 +27,7 @@ RSpec.describe Gitlab::Database::MigrationHelpers::CascadingNamespaceSettings do
it 'raises an error when some columns already exist' do
expect do
migration.add_cascading_namespace_setting(:cascading_setting, :integer)
- end.to raise_error %r/Existing columns: namespace_settings.cascading_setting, application_settings.lock_cascading_setting/
+ end.to raise_error %r{Existing columns: namespace_settings.cascading_setting, application_settings.lock_cascading_setting}
end
end
end
diff --git a/spec/lib/gitlab/database/migration_helpers/convert_to_bigint_spec.rb b/spec/lib/gitlab/database/migration_helpers/convert_to_bigint_spec.rb
index 1ff157b51d4..b0384a37746 100644
--- a/spec/lib/gitlab/database/migration_helpers/convert_to_bigint_spec.rb
+++ b/spec/lib/gitlab/database/migration_helpers/convert_to_bigint_spec.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe Gitlab::Database::MigrationHelpers::ConvertToBigint, feature_category: :database do
let(:migration) do
Class
- .new
+ .new(Gitlab::Database::Migration[2.1])
.include(described_class)
.include(Gitlab::Database::MigrationHelpers)
.new
@@ -73,4 +73,135 @@ RSpec.describe Gitlab::Database::MigrationHelpers::ConvertToBigint, feature_cate
expect(migration.columns_swapped?(:test_table, :id)).to eq(false)
end
end
+
+ describe '#add_bigint_column_indexes' do
+ let(:connection) { migration.connection }
+
+ let(:table_name) { '_test_table_bigint_indexes' }
+ let(:int_column) { 'token' }
+ let(:bigint_column) { 'token_convert_to_bigint' }
+
+ subject(:add_bigint_column_indexes) { migration.add_bigint_column_indexes(table_name, int_column) }
+
+ before do
+ connection.execute(<<~SQL)
+ CREATE TABLE IF NOT EXISTS public.#{table_name} (
+ name varchar(40),
+ #{int_column} integer
+ );
+ SQL
+
+ allow(migration).to receive(:transaction_open?).and_return(false)
+ allow(migration).to receive(:disable_statement_timeout).and_call_original
+ end
+
+ after do
+ connection.execute("DROP TABLE IF EXISTS #{table_name}")
+ end
+
+ context 'without corresponding bigint column' do
+ let(:error_msg) { "Bigint column '#{bigint_column}' does not exist on #{table_name}" }
+
+ it { expect { subject }.to raise_error(RuntimeError, error_msg) }
+ end
+
+ context 'with corresponding bigint column' do
+ let(:indexes) { connection.indexes(table_name) }
+ let(:int_column_indexes) { indexes.select { |i| i.columns.include?(int_column) } }
+ let(:bigint_column_indexes) { indexes.select { |i| i.columns.include?(bigint_column) } }
+
+ before do
+ connection.execute("ALTER TABLE #{table_name} ADD COLUMN #{bigint_column} bigint")
+ end
+
+ context 'without the integer column index' do
+ it 'does not create new bigint index' do
+ expect(int_column_indexes).to be_empty
+
+ add_bigint_column_indexes
+
+ expect(bigint_column_indexes).to be_empty
+ end
+ end
+
+ context 'with integer column indexes' do
+ let(:bigint_index_name) { ->(int_index_name) { migration.bigint_index_name(int_index_name) } }
+ let(:expected_bigint_indexes) do
+ [
+ {
+ name: bigint_index_name.call("hash_idx_#{table_name}"),
+ column: [bigint_column],
+ using: 'hash'
+ },
+ {
+ name: bigint_index_name.call("idx_#{table_name}"),
+ column: [bigint_column],
+ using: 'btree'
+ },
+ {
+ name: bigint_index_name.call("idx_#{table_name}_combined"),
+ column: "#{bigint_column}, lower((name)::text)",
+ where: "(#{bigint_column} IS NOT NULL)",
+ using: 'btree'
+ },
+ {
+ name: bigint_index_name.call("idx_#{table_name}_functional"),
+ column: "#{bigint_column}, lower((name)::text)",
+ using: 'btree'
+ },
+ {
+ name: bigint_index_name.call("idx_#{table_name}_ordered"),
+ column: [bigint_column],
+ order: 'DESC NULLS LAST',
+ using: 'btree'
+ },
+ {
+ name: bigint_index_name.call("idx_#{table_name}_ordered_multiple"),
+ column: [bigint_column, 'name'],
+ order: { bigint_column => 'DESC NULLS LAST', 'name' => 'desc' },
+ using: 'btree'
+ },
+ {
+ name: bigint_index_name.call("idx_#{table_name}_partial"),
+ column: [bigint_column],
+ where: "(#{bigint_column} IS NOT NULL)",
+ using: 'btree'
+ },
+ {
+ name: bigint_index_name.call("uniq_idx_#{table_name}"),
+ column: [bigint_column],
+ unique: true,
+ using: 'btree'
+ }
+ ]
+ end
+
+ before do
+ connection.execute(<<~SQL)
+ CREATE INDEX "hash_idx_#{table_name}" ON #{table_name} USING hash (#{int_column});
+ CREATE INDEX "idx_#{table_name}" ON #{table_name} USING btree (#{int_column});
+ CREATE INDEX "idx_#{table_name}_combined" ON #{table_name} USING btree (#{int_column}, lower((name)::text)) WHERE (#{int_column} IS NOT NULL);
+ CREATE INDEX "idx_#{table_name}_functional" ON #{table_name} USING btree (#{int_column}, lower((name)::text));
+ CREATE INDEX "idx_#{table_name}_ordered" ON #{table_name} USING btree (#{int_column} DESC NULLS LAST);
+ CREATE INDEX "idx_#{table_name}_ordered_multiple" ON #{table_name} USING btree (#{int_column} DESC NULLS LAST, name DESC);
+ CREATE INDEX "idx_#{table_name}_partial" ON #{table_name} USING btree (#{int_column}) WHERE (#{int_column} IS NOT NULL);
+ CREATE UNIQUE INDEX "uniq_idx_#{table_name}" ON #{table_name} USING btree (#{int_column});
+ SQL
+ end
+
+ it 'creates appropriate bigint indexes' do
+ expected_bigint_indexes.each do |bigint_index|
+ expect(migration).to receive(:add_concurrent_index).with(
+ table_name,
+ bigint_index[:column],
+ name: bigint_index[:name],
+ ** bigint_index.except(:name, :column)
+ )
+ end
+
+ add_bigint_column_indexes
+ end
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/database/migration_helpers/wraparound_autovacuum_spec.rb b/spec/lib/gitlab/database/migration_helpers/wraparound_autovacuum_spec.rb
index 1cc4ff6891c..b88d26100c9 100644
--- a/spec/lib/gitlab/database/migration_helpers/wraparound_autovacuum_spec.rb
+++ b/spec/lib/gitlab/database/migration_helpers/wraparound_autovacuum_spec.rb
@@ -14,20 +14,30 @@ RSpec.describe Gitlab::Database::MigrationHelpers::WraparoundAutovacuum, feature
describe '#can_execute_on?' do
using RSpec::Parameterized::TableSyntax
- where(:dot_com, :dev_or_test, :wraparound_prevention, :expectation) do
- true | true | true | false
- true | false | true | false
- false | true | true | false
- false | false | true | false
- true | true | false | true
- true | false | false | true
- false | true | false | true
- false | false | false | false
+ where(:dot_com, :jh, :dev_or_test, :wraparound_prevention, :expectation) do
+ true | true | true | true | false
+ true | true | false | true | false
+ false | true | true | true | false
+ false | true | false | true | false
+ true | true | true | false | true
+ true | true | false | false | false
+ false | true | true | false | true
+ false | true | false | false | false
+
+ true | false | true | true | false
+ true | false | false | true | false
+ false | false | true | true | false
+ false | false | false | true | false
+ true | false | true | false | true
+ true | false | false | false | true
+ false | false | true | false | true
+ false | false | false | false | false
end
with_them do
- it 'returns true for GitLab.com, dev, or test' do
+ it 'returns as expected for GitLab.com, dev, or test' do
allow(Gitlab).to receive(:com?).and_return(dot_com)
+ allow(Gitlab).to receive(:jh?).and_return(jh)
allow(Gitlab).to receive(:dev_or_test_env?).and_return(dev_or_test)
allow(migration).to receive(:wraparound_prevention_on_tables?).with([:table]).and_return(wraparound_prevention)
diff --git a/spec/lib/gitlab/database/migration_helpers_spec.rb b/spec/lib/gitlab/database/migration_helpers_spec.rb
index dd51cca688c..8bf05f56b3f 100644
--- a/spec/lib/gitlab/database/migration_helpers_spec.rb
+++ b/spec/lib/gitlab/database/migration_helpers_spec.rb
@@ -118,7 +118,7 @@ RSpec.describe Gitlab::Database::MigrationHelpers, feature_category: :database d
it 'cannot add unacceptable column names' do
expect do
model.add_timestamps_with_timezone(:foo, columns: [:bar])
- end.to raise_error %r/Illegal timestamp column name/
+ end.to raise_error %r{Illegal timestamp column name}
end
end
@@ -1753,8 +1753,8 @@ RSpec.describe Gitlab::Database::MigrationHelpers, feature_category: :database d
describe '#indexes_for' do
it 'returns the indexes for a column' do
- idx1 = double(:idx, columns: %w(project_id))
- idx2 = double(:idx, columns: %w(user_id))
+ idx1 = double(:idx, columns: %w[project_id])
+ idx2 = double(:idx, columns: %w[user_id])
allow(model).to receive(:indexes).with('table').and_return([idx1, idx2])
@@ -1777,7 +1777,7 @@ RSpec.describe Gitlab::Database::MigrationHelpers, feature_category: :database d
context 'when index name is too long' do
it 'does not fail' do
index = double(:index,
- columns: %w(uuid),
+ columns: %w[uuid],
name: 'index_vuln_findings_on_uuid_including_vuln_id_1',
using: nil,
where: nil,
@@ -1791,7 +1791,7 @@ RSpec.describe Gitlab::Database::MigrationHelpers, feature_category: :database d
expect(model).to receive(:add_concurrent_index)
.with(:vulnerability_occurrences,
- %w(tmp_undo_cleanup_column_8cbf300838),
+ %w[tmp_undo_cleanup_column_8cbf300838],
{
unique: true,
name: 'idx_copy_191a1af1a0',
@@ -1806,7 +1806,7 @@ RSpec.describe Gitlab::Database::MigrationHelpers, feature_category: :database d
context 'using a regular index using a single column' do
it 'copies the index' do
index = double(:index,
- columns: %w(project_id),
+ columns: %w[project_id],
name: 'index_on_issues_project_id',
using: nil,
where: nil,
@@ -1820,7 +1820,7 @@ RSpec.describe Gitlab::Database::MigrationHelpers, feature_category: :database d
expect(model).to receive(:add_concurrent_index)
.with(:issues,
- %w(gl_project_id),
+ %w[gl_project_id],
{
unique: false,
name: 'index_on_issues_gl_project_id',
@@ -1835,7 +1835,7 @@ RSpec.describe Gitlab::Database::MigrationHelpers, feature_category: :database d
context 'using a regular index with multiple columns' do
it 'copies the index' do
index = double(:index,
- columns: %w(project_id foobar),
+ columns: %w[project_id foobar],
name: 'index_on_issues_project_id_foobar',
using: nil,
where: nil,
@@ -1849,7 +1849,7 @@ RSpec.describe Gitlab::Database::MigrationHelpers, feature_category: :database d
expect(model).to receive(:add_concurrent_index)
.with(:issues,
- %w(gl_project_id foobar),
+ %w[gl_project_id foobar],
{
unique: false,
name: 'index_on_issues_gl_project_id_foobar',
@@ -1864,7 +1864,7 @@ RSpec.describe Gitlab::Database::MigrationHelpers, feature_category: :database d
context 'using an index with a WHERE clause' do
it 'copies the index' do
index = double(:index,
- columns: %w(project_id),
+ columns: %w[project_id],
name: 'index_on_issues_project_id',
using: nil,
where: 'foo',
@@ -1878,7 +1878,7 @@ RSpec.describe Gitlab::Database::MigrationHelpers, feature_category: :database d
expect(model).to receive(:add_concurrent_index)
.with(:issues,
- %w(gl_project_id),
+ %w[gl_project_id],
{
unique: false,
name: 'index_on_issues_gl_project_id',
@@ -1894,7 +1894,7 @@ RSpec.describe Gitlab::Database::MigrationHelpers, feature_category: :database d
context 'using an index with a USING clause' do
it 'copies the index' do
index = double(:index,
- columns: %w(project_id),
+ columns: %w[project_id],
name: 'index_on_issues_project_id',
where: nil,
using: 'foo',
@@ -1908,7 +1908,7 @@ RSpec.describe Gitlab::Database::MigrationHelpers, feature_category: :database d
expect(model).to receive(:add_concurrent_index)
.with(:issues,
- %w(gl_project_id),
+ %w[gl_project_id],
{
unique: false,
name: 'index_on_issues_gl_project_id',
@@ -1924,7 +1924,7 @@ RSpec.describe Gitlab::Database::MigrationHelpers, feature_category: :database d
context 'using an index with custom operator classes' do
it 'copies the index' do
index = double(:index,
- columns: %w(project_id),
+ columns: %w[project_id],
name: 'index_on_issues_project_id',
using: nil,
where: nil,
@@ -1938,7 +1938,7 @@ RSpec.describe Gitlab::Database::MigrationHelpers, feature_category: :database d
expect(model).to receive(:add_concurrent_index)
.with(:issues,
- %w(gl_project_id),
+ %w[gl_project_id],
{
unique: false,
name: 'index_on_issues_gl_project_id',
@@ -1955,7 +1955,7 @@ RSpec.describe Gitlab::Database::MigrationHelpers, feature_category: :database d
it 'copies the index' do
index = double(:index,
{
- columns: %w(project_id foobar),
+ columns: %w[project_id foobar],
name: 'index_on_issues_project_id_foobar',
using: :gin,
where: nil,
@@ -1970,7 +1970,7 @@ RSpec.describe Gitlab::Database::MigrationHelpers, feature_category: :database d
expect(model).to receive(:add_concurrent_index)
.with(:issues,
- %w(gl_project_id foobar),
+ %w[gl_project_id foobar],
{
unique: false,
name: 'index_on_issues_gl_project_id_foobar',
@@ -1988,7 +1988,7 @@ RSpec.describe Gitlab::Database::MigrationHelpers, feature_category: :database d
it 'copies the index' do
index = double(:index,
{
- columns: %w(project_id foobar),
+ columns: %w[project_id foobar],
name: 'index_on_issues_project_id_foobar',
using: :gin,
where: nil,
@@ -2003,7 +2003,7 @@ RSpec.describe Gitlab::Database::MigrationHelpers, feature_category: :database d
expect(model).to receive(:add_concurrent_index)
.with(:issues,
- %w(gl_project_id foobar),
+ %w[gl_project_id foobar],
{
unique: false,
name: 'index_on_issues_gl_project_id_foobar',
@@ -2020,7 +2020,7 @@ RSpec.describe Gitlab::Database::MigrationHelpers, feature_category: :database d
describe 'using an index of which the name does not contain the source column' do
it 'raises RuntimeError' do
index = double(:index,
- columns: %w(project_id),
+ columns: %w[project_id],
name: 'index_foobar_index',
using: nil,
where: nil,
diff --git a/spec/lib/gitlab/database/migrations/batched_background_migration_helpers_spec.rb b/spec/lib/gitlab/database/migrations/batched_background_migration_helpers_spec.rb
index f1271f2434c..a81ccf9583a 100644
--- a/spec/lib/gitlab/database/migrations/batched_background_migration_helpers_spec.rb
+++ b/spec/lib/gitlab/database/migrations/batched_background_migration_helpers_spec.rb
@@ -443,10 +443,10 @@ RSpec.describe Gitlab::Database::Migrations::BatchedBackgroundMigrationHelpers,
describe '#ensure_batched_background_migration_is_finished' do
let(:job_class_name) { 'CopyColumnUsingBackgroundMigrationJob' }
- let(:table_name) { 'events' }
+ let(:table_name) { '_test_table' }
let(:column_name) { :id }
let(:job_arguments) { [["id"], ["id_convert_to_bigint"], nil] }
- let(:gitlab_schema) { Gitlab::Database::GitlabSchema.table_schema!(table_name) }
+ let(:gitlab_schema) { :gitlab_main }
let(:configuration) do
{
@@ -484,7 +484,7 @@ RSpec.describe Gitlab::Database::Migrations::BatchedBackgroundMigrationHelpers,
"\n\n" \
"Finalize it manually by running the following command in a `bash` or `sh` shell:" \
"\n\n" \
- "\tsudo gitlab-rake gitlab:background_migrations:finalize[CopyColumnUsingBackgroundMigrationJob,events,id,'[[\"id\"]\\,[\"id_convert_to_bigint\"]\\,null]']" \
+ "\tsudo gitlab-rake gitlab:background_migrations:finalize[CopyColumnUsingBackgroundMigrationJob,_test_table,id,'[[\"id\"]\\,[\"id_convert_to_bigint\"]\\,null]']" \
"\n\n" \
"For more information, check the documentation" \
"\n\n" \
diff --git a/spec/lib/gitlab/database/migrations/constraints_helpers_spec.rb b/spec/lib/gitlab/database/migrations/constraints_helpers_spec.rb
index 476b5f3a784..4d7c51a3fbf 100644
--- a/spec/lib/gitlab/database/migrations/constraints_helpers_spec.rb
+++ b/spec/lib/gitlab/database/migrations/constraints_helpers_spec.rb
@@ -13,9 +13,11 @@ RSpec.describe Gitlab::Database::Migrations::ConstraintsHelpers do
describe '#check_constraint_name' do
it 'returns a valid constraint name' do
- name = model.check_constraint_name(:this_is_a_very_long_table_name,
- :with_a_very_long_column_name,
- :with_a_very_long_type)
+ name = model.check_constraint_name(
+ :this_is_a_very_long_table_name,
+ :with_a_very_long_column_name,
+ :with_a_very_long_type
+ )
expect(name).to be_an_instance_of(String)
expect(name).to start_with('check_')
@@ -404,9 +406,7 @@ RSpec.describe Gitlab::Database::Migrations::ConstraintsHelpers do
describe '#add_text_limit' do
context 'when it is called with the default options' do
it 'calls add_check_constraint with an infered constraint name and validate: true' do
- constraint_name = model.check_constraint_name(:test_table,
- :name,
- 'max_length')
+ constraint_name = model.check_constraint_name(:test_table, :name, 'max_length')
check = "char_length(name) <= 255"
expect(model).to receive(:check_constraint_name).and_call_original
@@ -440,9 +440,7 @@ RSpec.describe Gitlab::Database::Migrations::ConstraintsHelpers do
describe '#validate_text_limit' do
context 'when constraint_name is not provided' do
it 'calls validate_check_constraint with an infered constraint name' do
- constraint_name = model.check_constraint_name(:test_table,
- :name,
- 'max_length')
+ constraint_name = model.check_constraint_name(:test_table, :name, 'max_length')
expect(model).to receive(:check_constraint_name).and_call_original
expect(model).to receive(:validate_check_constraint)
@@ -468,9 +466,7 @@ RSpec.describe Gitlab::Database::Migrations::ConstraintsHelpers do
describe '#remove_text_limit' do
context 'when constraint_name is not provided' do
it 'calls remove_check_constraint with an infered constraint name' do
- constraint_name = model.check_constraint_name(:test_table,
- :name,
- 'max_length')
+ constraint_name = model.check_constraint_name(:test_table, :name, 'max_length')
expect(model).to receive(:check_constraint_name).and_call_original
expect(model).to receive(:remove_check_constraint)
@@ -496,9 +492,7 @@ RSpec.describe Gitlab::Database::Migrations::ConstraintsHelpers do
describe '#check_text_limit_exists?' do
context 'when constraint_name is not provided' do
it 'calls check_constraint_exists? with an infered constraint name' do
- constraint_name = model.check_constraint_name(:test_table,
- :name,
- 'max_length')
+ constraint_name = model.check_constraint_name(:test_table, :name, 'max_length')
expect(model).to receive(:check_constraint_name).and_call_original
expect(model).to receive(:check_constraint_exists?)
@@ -524,9 +518,7 @@ RSpec.describe Gitlab::Database::Migrations::ConstraintsHelpers do
describe '#add_not_null_constraint' do
context 'when it is called with the default options' do
it 'calls add_check_constraint with an infered constraint name and validate: true' do
- constraint_name = model.check_constraint_name(:test_table,
- :name,
- 'not_null')
+ constraint_name = model.check_constraint_name(:test_table, :name, 'not_null')
check = "name IS NOT NULL"
expect(model).to receive(:column_is_nullable?).and_return(true)
@@ -571,9 +563,7 @@ RSpec.describe Gitlab::Database::Migrations::ConstraintsHelpers do
describe '#validate_not_null_constraint' do
context 'when constraint_name is not provided' do
it 'calls validate_check_constraint with an infered constraint name' do
- constraint_name = model.check_constraint_name(:test_table,
- :name,
- 'not_null')
+ constraint_name = model.check_constraint_name(:test_table, :name, 'not_null')
expect(model).to receive(:check_constraint_name).and_call_original
expect(model).to receive(:validate_check_constraint)
@@ -599,9 +589,7 @@ RSpec.describe Gitlab::Database::Migrations::ConstraintsHelpers do
describe '#remove_not_null_constraint' do
context 'when constraint_name is not provided' do
it 'calls remove_check_constraint with an infered constraint name' do
- constraint_name = model.check_constraint_name(:test_table,
- :name,
- 'not_null')
+ constraint_name = model.check_constraint_name(:test_table, :name, 'not_null')
expect(model).to receive(:check_constraint_name).and_call_original
expect(model).to receive(:remove_check_constraint)
@@ -627,9 +615,7 @@ RSpec.describe Gitlab::Database::Migrations::ConstraintsHelpers do
describe '#check_not_null_constraint_exists?' do
context 'when constraint_name is not provided' do
it 'calls check_constraint_exists? with an infered constraint name' do
- constraint_name = model.check_constraint_name(:test_table,
- :name,
- 'not_null')
+ constraint_name = model.check_constraint_name(:test_table, :name, 'not_null')
expect(model).to receive(:check_constraint_name).and_call_original
expect(model).to receive(:check_constraint_exists?)
diff --git a/spec/lib/gitlab/database/migrations/instrumentation_spec.rb b/spec/lib/gitlab/database/migrations/instrumentation_spec.rb
index a12e0909dc2..81368dde94d 100644
--- a/spec/lib/gitlab/database/migrations/instrumentation_spec.rb
+++ b/spec/lib/gitlab/database/migrations/instrumentation_spec.rb
@@ -75,8 +75,12 @@ RSpec.describe Gitlab::Database::Migrations::Instrumentation do
context 'on successful execution' do
subject do
- instrumentation.observe(version: migration_version, name: migration_name,
- connection: connection, meta: migration_meta) {}
+ instrumentation.observe(
+ version: migration_version,
+ name: migration_name,
+ connection: connection,
+ meta: migration_meta
+ ) {}
end
it 'records a valid observation', :aggregate_failures do
@@ -98,8 +102,12 @@ RSpec.describe Gitlab::Database::Migrations::Instrumentation do
with_them do
subject(:observe) do
- instrumentation.observe(version: migration_version, name: migration_name,
- connection: connection, meta: migration_meta) { raise exception, error_message }
+ instrumentation.observe(
+ version: migration_version,
+ name: migration_name,
+ connection: connection,
+ meta: migration_meta
+ ) { raise exception, error_message }
end
context 'retrieving observations' do
diff --git a/spec/lib/gitlab/database/migrations/milestone_mixin_spec.rb b/spec/lib/gitlab/database/migrations/milestone_mixin_spec.rb
index e375af494a2..1ed5c846550 100644
--- a/spec/lib/gitlab/database/migrations/milestone_mixin_spec.rb
+++ b/spec/lib/gitlab/database/migrations/milestone_mixin_spec.rb
@@ -12,14 +12,11 @@ RSpec.describe Gitlab::Database::Migrations::MilestoneMixin, feature_category: :
end
let(:migration_mixin) do
- Class.new(Gitlab::Database::Migration[2.1]) do
- include Gitlab::Database::Migrations::MilestoneMixin
- end
+ Class.new(Gitlab::Database::Migration[2.2])
end
let(:migration_mixin_version) do
- Class.new(Gitlab::Database::Migration[2.1]) do
- include Gitlab::Database::Migrations::MilestoneMixin
+ Class.new(Gitlab::Database::Migration[2.2]) do
milestone '16.4'
end
end
@@ -44,5 +41,11 @@ RSpec.describe Gitlab::Database::Migrations::MilestoneMixin, feature_category: :
expect { migration_mixin_version.new(4, 4, :regular) }.not_to raise_error
end
end
+
+ context 'when initialize arguments are not provided' do
+ it "does not raise an error" do
+ expect { migration_mixin_version.new }.not_to raise_error
+ end
+ end
end
end
diff --git a/spec/lib/gitlab/database/migrations/reestablished_connection_stack_spec.rb b/spec/lib/gitlab/database/migrations/reestablished_connection_stack_spec.rb
index c6327de98d1..6e943307ae6 100644
--- a/spec/lib/gitlab/database/migrations/reestablished_connection_stack_spec.rb
+++ b/spec/lib/gitlab/database/migrations/reestablished_connection_stack_spec.rb
@@ -31,7 +31,7 @@ RSpec.describe Gitlab::Database::Migrations::ReestablishedConnectionStack do
# establish connection
ApplicationRecord.connection.select_one("SELECT 1 FROM projects LIMIT 1")
- Ci::ApplicationRecord.connection.select_one("SELECT 1 FROM ci_builds LIMIT 1")
+ Ci::ApplicationRecord.connection.select_one("SELECT 1 FROM p_ci_builds LIMIT 1")
end
expect(new_handler).not_to eq(original_handler), "is reconnected"
diff --git a/spec/lib/gitlab/database/migrations/test_batched_background_runner_spec.rb b/spec/lib/gitlab/database/migrations/test_batched_background_runner_spec.rb
index 31a194ae883..660c4347ffa 100644
--- a/spec/lib/gitlab/database/migrations/test_batched_background_runner_spec.rb
+++ b/spec/lib/gitlab/database/migrations/test_batched_background_runner_spec.rb
@@ -80,8 +80,11 @@ RSpec.describe Gitlab::Database::Migrations::TestBatchedBackgroundRunner, :freez
end
subject(:sample_migration) do
- described_class.new(result_dir: result_dir, connection: connection,
- from_id: from_id).run_jobs(for_duration: 1.minute)
+ described_class.new(
+ result_dir: result_dir,
+ connection: connection,
+ from_id: from_id
+ ).run_jobs(for_duration: 1.minute)
end
it 'runs sampled jobs from the batched background migration' do
@@ -125,12 +128,19 @@ RSpec.describe Gitlab::Database::Migrations::TestBatchedBackgroundRunner, :freez
calls << args
end
- queue_migration(migration_name, table_name, :id,
- job_interval: 5.minutes,
- batch_size: 100)
+ queue_migration(
+ migration_name,
+ table_name,
+ :id,
+ job_interval: 5.minutes,
+ batch_size: 100
+ )
- described_class.new(result_dir: result_dir, connection: connection,
- from_id: from_id).run_jobs(for_duration: 3.minutes)
+ described_class.new(
+ result_dir: result_dir,
+ connection: connection,
+ from_id: from_id
+ ).run_jobs(for_duration: 3.minutes)
expect(calls).not_to be_empty
end
@@ -142,13 +152,19 @@ RSpec.describe Gitlab::Database::Migrations::TestBatchedBackgroundRunner, :freez
calls << args
end
- queue_migration(migration_name, table_name, :id,
- job_interval: 5.minutes,
- batch_size: num_rows_in_table * 2,
- sub_batch_size: num_rows_in_table * 2)
+ queue_migration(
+ migration_name,
+ table_name, :id,
+ job_interval: 5.minutes,
+ batch_size: num_rows_in_table * 2,
+ sub_batch_size: num_rows_in_table * 2
+ )
- described_class.new(result_dir: result_dir, connection: connection,
- from_id: from_id).run_jobs(for_duration: 3.minutes)
+ described_class.new(
+ result_dir: result_dir,
+ connection: connection,
+ from_id: from_id
+ ).run_jobs(for_duration: 3.minutes)
expect(calls.size).to eq(1)
end
@@ -161,13 +177,20 @@ RSpec.describe Gitlab::Database::Migrations::TestBatchedBackgroundRunner, :freez
calls << args
end
- queue_migration(migration_name, table_name, :id,
- job_interval: 5.minutes,
- batch_size: num_rows_in_table * 2,
- sub_batch_size: num_rows_in_table * 2)
-
- described_class.new(result_dir: result_dir, connection: connection,
- from_id: from_id).run_jobs(for_duration: 3.minutes)
+ queue_migration(
+ migration_name,
+ table_name,
+ :id,
+ job_interval: 5.minutes,
+ batch_size: num_rows_in_table * 2,
+ sub_batch_size: num_rows_in_table * 2
+ )
+
+ described_class.new(
+ result_dir: result_dir,
+ connection: connection,
+ from_id: from_id
+ ).run_jobs(for_duration: 3.minutes)
expect(calls.count).to eq(0)
end
@@ -181,26 +204,41 @@ RSpec.describe Gitlab::Database::Migrations::TestBatchedBackgroundRunner, :freez
it 'runs all pending jobs based on the last migration id' do
old_migration = define_background_migration(migration_name)
- queue_migration(migration_name, table_name, :id,
- job_interval: 5.minutes,
- batch_size: 100)
+ queue_migration(
+ migration_name,
+ table_name,
+ :id,
+ job_interval: 5.minutes,
+ batch_size: 100
+ )
last_id
new_migration = define_background_migration('NewMigration') { travel 1.second }
- queue_migration('NewMigration', table_name, :id,
- job_interval: 5.minutes,
- batch_size: 10,
- sub_batch_size: 5)
+ queue_migration(
+ 'NewMigration',
+ table_name,
+ :id,
+ job_interval: 5.minutes,
+ batch_size: 10,
+ sub_batch_size: 5
+ )
other_new_migration = define_background_migration('NewMigration2') { travel 2.seconds }
- queue_migration('NewMigration2', table_name, :id,
- job_interval: 5.minutes,
- batch_size: 10,
- sub_batch_size: 5)
+ queue_migration(
+ 'NewMigration2',
+ table_name,
+ :id,
+ job_interval: 5.minutes,
+ batch_size: 10,
+ sub_batch_size: 5
+ )
expect_migration_runs(new_migration => 3, other_new_migration => 2, old_migration => 0) do
- described_class.new(result_dir: result_dir, connection: connection,
- from_id: last_id).run_jobs(for_duration: 5.seconds)
+ described_class.new(
+ result_dir: result_dir,
+ connection: connection,
+ from_id: last_id
+ ).run_jobs(for_duration: 5.seconds)
end
end
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
index c6cd5e55754..c57b8bb5992 100644
--- a/spec/lib/gitlab/database/no_cross_db_foreign_keys_spec.rb
+++ b/spec/lib/gitlab/database/no_cross_db_foreign_keys_spec.rb
@@ -11,6 +11,7 @@ RSpec.describe 'cross-database foreign keys' do
# should be added as a comment along with the name of the column.
let!(:allowed_cross_database_foreign_keys) do
[
+ 'events.author_id', # https://gitlab.com/gitlab-org/gitlab/-/issues/429803
'gitlab_subscriptions.hosted_plan_id', # https://gitlab.com/gitlab-org/gitlab/-/issues/422012
'group_import_states.user_id', # https://gitlab.com/gitlab-org/gitlab/-/issues/421210
'identities.saml_provider_id', # https://gitlab.com/gitlab-org/gitlab/-/issues/422010
@@ -18,11 +19,18 @@ RSpec.describe 'cross-database foreign keys' do
'issues.closed_by_id', # https://gitlab.com/gitlab-org/gitlab/-/issues/422154
'issues.updated_by_id', # https://gitlab.com/gitlab-org/gitlab/-/issues/422154
'issue_assignees.user_id', # https://gitlab.com/gitlab-org/gitlab/-/issues/422154
+ 'lfs_file_locks.user_id', # https://gitlab.com/gitlab-org/gitlab/-/issues/430838
'merge_requests.assignee_id', # https://gitlab.com/gitlab-org/gitlab/-/issues/422080
'merge_requests.updated_by_id', # https://gitlab.com/gitlab-org/gitlab/-/issues/422080
'merge_requests.merge_user_id', # https://gitlab.com/gitlab-org/gitlab/-/issues/422080
'merge_requests.author_id', # https://gitlab.com/gitlab-org/gitlab/-/issues/422080
+ 'namespace_commit_emails.email_id', # https://gitlab.com/gitlab-org/gitlab/-/issues/429804
+ 'namespace_commit_emails.user_id', # https://gitlab.com/gitlab-org/gitlab/-/issues/429804
+ 'path_locks.user_id', # https://gitlab.com/gitlab-org/gitlab/-/issues/429380
'project_authorizations.user_id', # https://gitlab.com/gitlab-org/gitlab/-/issues/422044
+ 'protected_branch_push_access_levels.user_id', # https://gitlab.com/gitlab-org/gitlab/-/issues/431054
+ 'protected_branch_merge_access_levels.user_id', # https://gitlab.com/gitlab-org/gitlab/-/issues/431055
+ 'security_orchestration_policy_configurations.bot_user_id', # https://gitlab.com/gitlab-org/gitlab/-/issues/429438
'user_group_callouts.user_id' # https://gitlab.com/gitlab-org/gitlab/-/issues/421287
]
end
diff --git a/spec/lib/gitlab/database/no_new_tables_with_gitlab_main_schema_spec.rb b/spec/lib/gitlab/database/no_new_tables_with_gitlab_main_schema_spec.rb
new file mode 100644
index 00000000000..338475fa9c4
--- /dev/null
+++ b/spec/lib/gitlab/database/no_new_tables_with_gitlab_main_schema_spec.rb
@@ -0,0 +1,63 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'new tables with gitlab_main schema', feature_category: :cell do
+ # During the development of Cells, we will be moving tables from the `gitlab_main` schema
+ # to either the `gitlab_main_clusterwide` or `gitlab_main_cell` schema.
+ # As part of this process, starting from milestone 16.7, it will be a mandatory requirement that
+ # all newly created tables are associated with one of these two schemas.
+ # Any attempt to set the `gitlab_main` schema for a new table will result in a failure of this spec.
+
+ # Specific tables can be exempted from this requirement, and such tables must be added to the `exempted_tables` list.
+ let!(:exempted_tables) do
+ []
+ end
+
+ let!(:starting_from_milestone) { 16.7 }
+
+ it 'only allows exempted tables to have `gitlab_main` as its schema, after milestone 16.7', :aggregate_failures do
+ tables_having_gitlab_main_schema(starting_from_milestone: starting_from_milestone).each do |table_name|
+ expect(exempted_tables).to include(table_name), error_message(table_name)
+ end
+ end
+
+ it 'only allows tables having `gitlab_main` as its schema in `exempted_tables`', :aggregate_failures do
+ tables_having_gitlab_main_schema = gitlab_main_schema_tables.map(&:table_name)
+
+ exempted_tables.each do |exempted_table|
+ expect(tables_having_gitlab_main_schema).to include(exempted_table),
+ "`#{exempted_table}` does not have `gitlab_main` as its schema.
+ Please remove this table from the `exempted_tables` list."
+ end
+ end
+
+ private
+
+ def error_message(table_name)
+ <<~HEREDOC
+ The table `#{table_name}` has been added with `gitlab_main` schema.
+ Starting from GitLab #{starting_from_milestone}, we expect new tables to use either the `gitlab_main_cell` or the
+ `gitlab_main_clusterwide` schema.
+
+ To choose an appropriate schema for this table from among `gitlab_main_cell` and `gitlab_main_clusterwide`, please refer
+ to our guidelines at https://docs.gitlab.com/ee/development/database/multiple_databases.html#guidelines-on-choosing-between-gitlab_main_cell-and-gitlab_main_clusterwide-schema, or consult with the Tenant Scale group.
+
+ Please see issue https://gitlab.com/gitlab-org/gitlab/-/issues/424990 to understand why this change is being enforced.
+ HEREDOC
+ end
+
+ def tables_having_gitlab_main_schema(starting_from_milestone:)
+ selected_data = gitlab_main_schema_tables.select do |database_dictionary|
+ database_dictionary.milestone.to_f >= starting_from_milestone
+ end
+
+ selected_data.map(&:table_name)
+ end
+
+ def gitlab_main_schema_tables
+ ::Gitlab::Database::GitlabSchema.build_dictionary('').select do |database_dictionary|
+ database_dictionary.schema?('gitlab_main')
+ end
+ end
+end
diff --git a/spec/lib/gitlab/database/partitioning/detached_partition_dropper_spec.rb b/spec/lib/gitlab/database/partitioning/detached_partition_dropper_spec.rb
index 04940028aee..eb78d836be0 100644
--- a/spec/lib/gitlab/database/partitioning/detached_partition_dropper_spec.rb
+++ b/spec/lib/gitlab/database/partitioning/detached_partition_dropper_spec.rb
@@ -57,17 +57,19 @@ RSpec.describe Gitlab::Database::Partitioning::DetachedPartitionDropper do
SQL
end
- Postgresql::DetachedPartition.create!(table_name: name,
- drop_after: drop_after)
+ Postgresql::DetachedPartition.create!(table_name: name, drop_after: drop_after)
end
describe '#perform' do
context 'when the partition should not be dropped yet' do
it 'does not drop the partition' do
- create_partition(name: :_test_partition,
- from: 2.months.ago, to: 1.month.ago,
- attached: false,
- drop_after: 1.day.from_now)
+ create_partition(
+ name: :_test_partition,
+ from: 2.months.ago,
+ to: 1.month.ago,
+ attached: false,
+ drop_after: 1.day.from_now
+ )
dropper.perform
@@ -77,11 +79,13 @@ RSpec.describe Gitlab::Database::Partitioning::DetachedPartitionDropper do
context 'with a partition to drop' do
before do
- create_partition(name: :_test_partition,
- from: 2.months.ago,
- to: 1.month.ago.beginning_of_month,
- attached: false,
- drop_after: 1.second.ago)
+ create_partition(
+ name: :_test_partition,
+ from: 2.months.ago,
+ to: 1.month.ago.beginning_of_month,
+ attached: false,
+ drop_after: 1.second.ago
+ )
end
it 'drops the partition' do
@@ -159,11 +163,13 @@ RSpec.describe Gitlab::Database::Partitioning::DetachedPartitionDropper do
context 'when the partition to drop is still attached to its table' do
before do
- create_partition(name: :_test_partition,
- from: 2.months.ago,
- to: 1.month.ago.beginning_of_month,
- attached: true,
- drop_after: 1.second.ago)
+ create_partition(
+ name: :_test_partition,
+ from: 2.months.ago,
+ to: 1.month.ago.beginning_of_month,
+ attached: true,
+ drop_after: 1.second.ago
+ )
end
it 'does not drop the partition, but does remove the DetachedPartition entry' do
@@ -192,17 +198,21 @@ RSpec.describe Gitlab::Database::Partitioning::DetachedPartitionDropper do
context 'with multiple partitions to drop' do
before do
- create_partition(name: :_test_partition_1,
- from: 3.months.ago,
- to: 2.months.ago,
- attached: false,
- drop_after: 1.second.ago)
-
- create_partition(name: :_test_partition_2,
- from: 2.months.ago,
- to: 1.month.ago,
- attached: false,
- drop_after: 1.second.ago)
+ create_partition(
+ name: :_test_partition_1,
+ from: 3.months.ago,
+ to: 2.months.ago,
+ attached: false,
+ drop_after: 1.second.ago
+ )
+
+ create_partition(
+ name: :_test_partition_2,
+ from: 2.months.ago,
+ to: 1.month.ago,
+ attached: false,
+ drop_after: 1.second.ago
+ )
end
it 'drops both partitions' do
diff --git a/spec/lib/gitlab/database/partitioning/monthly_strategy_spec.rb b/spec/lib/gitlab/database/partitioning/monthly_strategy_spec.rb
index 3afa338fdf7..8b18e5b6d08 100644
--- a/spec/lib/gitlab/database/partitioning/monthly_strategy_spec.rb
+++ b/spec/lib/gitlab/database/partitioning/monthly_strategy_spec.rb
@@ -235,8 +235,12 @@ RSpec.describe Gitlab::Database::Partitioning::MonthlyStrategy, feature_category
subject { described_class.new(model, partitioning_key, retain_for: 3.months).extra_partitions }
it 'prunes the unbounded partition ending 2020-05-01' do
- min_value_to_may = Gitlab::Database::Partitioning::TimePartition.new(model.table_name, nil, '2020-05-01',
- partition_name: '_test_partitioned_test_000000')
+ min_value_to_may = Gitlab::Database::Partitioning::TimePartition.new(
+ model.table_name,
+ nil,
+ '2020-05-01',
+ partition_name: '_test_partitioned_test_000000'
+ )
expect(subject).to contain_exactly(min_value_to_may)
end
@@ -247,8 +251,18 @@ RSpec.describe Gitlab::Database::Partitioning::MonthlyStrategy, feature_category
it 'prunes the unbounded partition and the partition for May-June' do
expect(subject).to contain_exactly(
- Gitlab::Database::Partitioning::TimePartition.new(model.table_name, nil, '2020-05-01', partition_name: '_test_partitioned_test_000000'),
- Gitlab::Database::Partitioning::TimePartition.new(model.table_name, '2020-05-01', '2020-06-01', partition_name: '_test_partitioned_test_202005')
+ Gitlab::Database::Partitioning::TimePartition.new(
+ model.table_name,
+ nil,
+ '2020-05-01',
+ partition_name: '_test_partitioned_test_000000'
+ ),
+ Gitlab::Database::Partitioning::TimePartition.new(
+ model.table_name,
+ '2020-05-01',
+ '2020-06-01',
+ partition_name: '_test_partitioned_test_202005'
+ )
)
end
@@ -257,8 +271,18 @@ RSpec.describe Gitlab::Database::Partitioning::MonthlyStrategy, feature_category
it 'prunes empty partitions' do
expect(subject).to contain_exactly(
- Gitlab::Database::Partitioning::TimePartition.new(model.table_name, nil, '2020-05-01', partition_name: '_test_partitioned_test_000000'),
- Gitlab::Database::Partitioning::TimePartition.new(model.table_name, '2020-05-01', '2020-06-01', partition_name: '_test_partitioned_test_202005')
+ Gitlab::Database::Partitioning::TimePartition.new(
+ model.table_name,
+ nil,
+ '2020-05-01',
+ partition_name: '_test_partitioned_test_000000'
+ ),
+ Gitlab::Database::Partitioning::TimePartition.new(
+ model.table_name,
+ '2020-05-01',
+ '2020-06-01',
+ partition_name: '_test_partitioned_test_202005'
+ )
)
end
diff --git a/spec/lib/gitlab/database/partitioning/partition_manager_spec.rb b/spec/lib/gitlab/database/partitioning/partition_manager_spec.rb
index 80ffa708d8a..336cd2d912d 100644
--- a/spec/lib/gitlab/database/partitioning/partition_manager_spec.rb
+++ b/spec/lib/gitlab/database/partitioning/partition_manager_spec.rb
@@ -6,6 +6,7 @@ RSpec.describe Gitlab::Database::Partitioning::PartitionManager, feature_categor
include ActiveSupport::Testing::TimeHelpers
include Database::PartitioningHelpers
include ExclusiveLeaseHelpers
+ using RSpec::Parameterized::TableSyntax
let(:partitioned_table_name) { :_test_gitlab_main_my_model_example_table }
@@ -107,14 +108,88 @@ RSpec.describe Gitlab::Database::Partitioning::PartitionManager, feature_categor
end
end
- before do
- my_model.table_name = partitioned_table_name
+ context 'when single database is configured' do
+ before do
+ skip_if_database_exists(:ci)
- create_partitioned_table(connection, partitioned_table_name)
+ my_model.table_name = partitioned_table_name
+
+ create_partitioned_table(connection, partitioned_table_name)
+ end
+
+ it 'creates partitions' do
+ expect { sync_partitions }.to change { find_partitions(my_model.table_name, schema: Gitlab::Database::DYNAMIC_PARTITIONS_SCHEMA).size }.from(0)
+ end
end
- it 'creates partitions' do
- expect { sync_partitions }.to change { find_partitions(my_model.table_name, schema: Gitlab::Database::DYNAMIC_PARTITIONS_SCHEMA).size }.from(0)
+ context 'when multiple databases are configured' do
+ before do
+ skip_if_shared_database(:ci)
+
+ my_model.table_name = partitioned_table_name
+
+ create_partitioned_table(connection, partitioned_table_name)
+
+ stub_feature_flags(automatic_lock_writes_on_partition_tables: ff_enabled)
+
+ sync_partitions
+ end
+
+ where(:gitlab_schema, :database, :expectation) do
+ :gitlab_main | :main | false
+ :gitlab_main | :ci | true
+ :gitlab_ci | :main | true
+ :gitlab_ci | :ci | false
+ end
+ with_them do
+ subject(:sync_partitions) { described_class.new(my_model, connection: connection).sync_partitions }
+
+ let(:partitioned_table_name) { "_test_gitlab_#{database}_my_model_example_#{gitlab_schema}" }
+ let(:base_model) { Gitlab::Database.schemas_to_base_models[gitlab_schema].first }
+ let(:connection) { Gitlab::Database.database_base_models[database.to_s].connection }
+
+ let(:my_model) do
+ Class.new(base_model) do
+ include PartitionedTable
+
+ partitioned_by :created_at, strategy: :monthly
+ end
+ end
+
+ let(:partitions) do
+ Gitlab::Database::PostgresPartition.using_connection(connection) { Gitlab::Database::PostgresPartition.for_parent_table(partitioned_table_name).to_a }
+ end
+
+ let(:partitions_locked_for_writes?) do
+ partitions.map do |partition|
+ Gitlab::Database::LockWritesManager.new(
+ table_name: "#{partition.schema}.#{partition.name}",
+ connection: connection,
+ database_name: gitlab_schema
+ ).table_locked_for_writes?
+ end.all?(true)
+ end
+
+ context 'when feature flag is enabled' do
+ let(:ff_enabled) { true }
+
+ it "matches expectation" do
+ sync_partitions
+
+ expect(partitions_locked_for_writes?).to eq(expectation)
+ end
+ end
+
+ context 'when feature flag is disabled' do
+ let(:ff_enabled) { false }
+
+ it "will not lock created partition" do
+ sync_partitions
+
+ expect(partitions_locked_for_writes?).to eq(false)
+ end
+ end
+ end
end
end
diff --git a/spec/lib/gitlab/database/partitioning/sliding_list_strategy_spec.rb b/spec/lib/gitlab/database/partitioning/sliding_list_strategy_spec.rb
index ac4d345271e..9ca0a1b6e57 100644
--- a/spec/lib/gitlab/database/partitioning/sliding_list_strategy_spec.rb
+++ b/spec/lib/gitlab/database/partitioning/sliding_list_strategy_spec.rb
@@ -15,9 +15,12 @@ RSpec.describe Gitlab::Database::Partitioning::SlidingListStrategy, feature_cate
let(:detach_partition_if) { double('detach_partition_if') }
subject(:strategy) do
- described_class.new(model, :partition,
- next_partition_if: next_partition_if,
- detach_partition_if: detach_partition_if)
+ described_class.new(
+ model,
+ :partition,
+ next_partition_if: next_partition_if,
+ detach_partition_if: detach_partition_if
+ )
end
before do
@@ -213,9 +216,9 @@ RSpec.describe Gitlab::Database::Partitioning::SlidingListStrategy, feature_cate
include PartitionedTable
partitioned_by :partition,
- strategy: :sliding_list,
- next_partition_if: proc { false },
- detach_partition_if: proc { false }
+ strategy: :sliding_list,
+ next_partition_if: proc { false },
+ detach_partition_if: proc { false }
end
end.to raise_error(/ignored_columns/)
end
@@ -228,9 +231,9 @@ RSpec.describe Gitlab::Database::Partitioning::SlidingListStrategy, feature_cate
self.ignored_columns = [:partition]
partitioned_by :partition,
- strategy: :sliding_list,
- next_partition_if: proc { false },
- detach_partition_if: proc { false }
+ strategy: :sliding_list,
+ next_partition_if: proc { false },
+ detach_partition_if: proc { false }
end
end.not_to raise_error
end
@@ -255,9 +258,9 @@ RSpec.describe Gitlab::Database::Partitioning::SlidingListStrategy, feature_cate
detach_partition?(...)
end
partitioned_by :partition,
- strategy: :sliding_list,
- next_partition_if: method(:next_partition_if_wrapper),
- detach_partition_if: method(:detach_partition_if_wrapper)
+ strategy: :sliding_list,
+ next_partition_if: method(:next_partition_if_wrapper),
+ detach_partition_if: method(:detach_partition_if_wrapper)
def self.next_partition?(current_partition); end
diff --git a/spec/lib/gitlab/database/partitioning_migration_helpers/index_helpers_spec.rb b/spec/lib/gitlab/database/partitioning_migration_helpers/index_helpers_spec.rb
index a81c8a5a49c..aa644885306 100644
--- a/spec/lib/gitlab/database/partitioning_migration_helpers/index_helpers_spec.rb
+++ b/spec/lib/gitlab/database/partitioning_migration_helpers/index_helpers_spec.rb
@@ -99,8 +99,11 @@ RSpec.describe Gitlab::Database::PartitioningMigrationHelpers::IndexHelpers do
expect(migration).to receive(:add_index)
.with(table_name, column_name, { name: index_name, where: 'x > 0', unique: true })
- migration.add_concurrent_partitioned_index(table_name, column_name,
- { name: index_name, where: 'x > 0', unique: true })
+ migration.add_concurrent_partitioned_index(
+ table_name,
+ column_name,
+ { name: index_name, where: 'x > 0', unique: true }
+ )
end
end
diff --git a/spec/lib/gitlab/database/partitioning_migration_helpers/table_management_helpers_spec.rb b/spec/lib/gitlab/database/partitioning_migration_helpers/table_management_helpers_spec.rb
index 6a947044317..31c669ff330 100644
--- a/spec/lib/gitlab/database/partitioning_migration_helpers/table_management_helpers_spec.rb
+++ b/spec/lib/gitlab/database/partitioning_migration_helpers/table_management_helpers_spec.rb
@@ -2,6 +2,173 @@
require 'spec_helper'
+RSpec.shared_examples "a measurable object" do
+ context 'when the table is not allowed' do
+ let(:source_table) { :_test_this_table_is_not_allowed }
+
+ it 'raises an error' do
+ expect(migration).to receive(:assert_table_is_allowed).with(source_table).and_call_original
+
+ expect do
+ subject
+ end.to raise_error(/#{source_table} is not allowed for use/)
+ end
+ end
+
+ context 'when run inside a transaction block' do
+ it 'raises an error' do
+ expect(migration).to receive(:transaction_open?).and_return(true)
+
+ expect do
+ subject
+ end.to raise_error(/can not be run inside a transaction/)
+ end
+ end
+
+ context 'when the given table does not have a primary key' do
+ it 'raises an error' do
+ migration.execute(<<~SQL)
+ ALTER TABLE #{source_table}
+ DROP CONSTRAINT #{source_table}_pkey
+ SQL
+
+ expect do
+ subject
+ end.to raise_error(/primary key not defined for #{source_table}/)
+ end
+ end
+
+ it 'creates the partitioned table with the same non-key columns' do
+ subject
+
+ copied_columns = filter_columns_by_name(connection.columns(partitioned_table), new_primary_key)
+ original_columns = filter_columns_by_name(connection.columns(source_table), new_primary_key)
+
+ expect(copied_columns).to match_array(original_columns)
+ end
+
+ it 'removes the default from the primary key column' do
+ subject
+
+ pk_column = connection.columns(partitioned_table).find { |c| c.name == old_primary_key }
+
+ expect(pk_column.default_function).to be_nil
+ end
+
+ describe 'constructing the partitioned table' do
+ it 'creates a table partitioned by the proper column' do
+ subject
+
+ expect(connection.table_exists?(partitioned_table)).to be(true)
+ expect(connection.primary_key(partitioned_table)).to eq(new_primary_key)
+
+ expect_table_partitioned_by(partitioned_table, [partition_column_name])
+ end
+
+ it 'requires the migration helper to be run in DDL mode' do
+ expect(Gitlab::Database::QueryAnalyzers::RestrictAllowedSchemas).to receive(:require_ddl_mode!)
+
+ subject
+
+ expect(connection.table_exists?(partitioned_table)).to be(true)
+ expect(connection.primary_key(partitioned_table)).to eq(new_primary_key)
+
+ expect_table_partitioned_by(partitioned_table, [partition_column_name])
+ end
+
+ it 'changes the primary key datatype to bigint' do
+ subject
+
+ pk_column = connection.columns(partitioned_table).find { |c| c.name == old_primary_key }
+
+ expect(pk_column.sql_type).to eq('bigint')
+ end
+
+ it 'removes the default from the primary key column' do
+ subject
+
+ pk_column = connection.columns(partitioned_table).find { |c| c.name == old_primary_key }
+
+ expect(pk_column.default_function).to be_nil
+ end
+
+ it 'creates the partitioned table with the same non-key columns' do
+ subject
+
+ copied_columns = filter_columns_by_name(connection.columns(partitioned_table), new_primary_key)
+ original_columns = filter_columns_by_name(connection.columns(source_table), new_primary_key)
+
+ expect(copied_columns).to match_array(original_columns)
+ end
+ end
+
+ describe 'keeping data in sync with the partitioned table' do
+ before do
+ partitioned_model.primary_key = :id
+ partitioned_model.table_name = partitioned_table
+ end
+
+ it 'creates a trigger function on the original table' do
+ expect_function_not_to_exist(function_name)
+ expect_trigger_not_to_exist(source_table, trigger_name)
+
+ subject
+
+ expect_function_to_exist(function_name)
+ expect_valid_function_trigger(source_table, trigger_name, function_name, after: %w[delete insert update])
+ end
+
+ it 'syncs inserts to the partitioned tables' do
+ subject
+
+ expect(partitioned_model.count).to eq(0)
+
+ first_record = source_model.create!(name: 'Bob', age: 20, created_at: timestamp, external_id: 1, updated_at: timestamp)
+ second_record = source_model.create!(name: 'Alice', age: 30, created_at: timestamp, external_id: 2, updated_at: timestamp)
+
+ expect(partitioned_model.count).to eq(2)
+ expect(partitioned_model.find(first_record.id).attributes).to eq(first_record.attributes)
+ expect(partitioned_model.find(second_record.id).attributes).to eq(second_record.attributes)
+ end
+
+ it 'syncs updates to the partitioned tables' do
+ subject
+
+ first_record = source_model.create!(name: 'Bob', age: 20, created_at: timestamp, external_id: 1, updated_at: timestamp)
+ second_record = source_model.create!(name: 'Alice', age: 30, created_at: timestamp, external_id: 2, updated_at: timestamp)
+
+ expect(partitioned_model.count).to eq(2)
+
+ first_copy = partitioned_model.find(first_record.id)
+ second_copy = partitioned_model.find(second_record.id)
+
+ expect(first_copy.attributes).to eq(first_record.attributes)
+ expect(second_copy.attributes).to eq(second_record.attributes)
+
+ first_record.update!(age: 21, updated_at: timestamp + 1.hour, external_id: 3)
+
+ expect(partitioned_model.count).to eq(2)
+ expect(first_copy.reload.attributes).to eq(first_record.attributes)
+ expect(second_copy.reload.attributes).to eq(second_record.attributes)
+ end
+
+ it 'syncs deletes to the partitioned tables' do
+ subject
+
+ first_record = source_model.create!(name: 'Bob', age: 20, created_at: timestamp, external_id: 1, updated_at: timestamp)
+ second_record = source_model.create!(name: 'Alice', age: 30, created_at: timestamp, external_id: 2, updated_at: timestamp)
+
+ expect(partitioned_model.count).to eq(2)
+
+ first_record.destroy!
+
+ expect(partitioned_model.count).to eq(1)
+ expect(partitioned_model.find_by_id(first_record.id)).to be_nil
+ expect(partitioned_model.find(second_record.id).attributes).to eq(second_record.attributes)
+ end
+ end
+end
+
RSpec.describe Gitlab::Database::PartitioningMigrationHelpers::TableManagementHelpers, feature_category: :database do
include Database::PartitioningHelpers
include Database::TriggerHelpers
@@ -18,6 +185,7 @@ RSpec.describe Gitlab::Database::PartitioningMigrationHelpers::TableManagementHe
let(:partitioned_table) { :_test_migration_partitioned_table }
let(:function_name) { :_test_migration_function_name }
let(:trigger_name) { :_test_migration_trigger_name }
+ let(:partition_column2) { 'external_id' }
let(:partition_column) { 'created_at' }
let(:min_date) { Date.new(2019, 12) }
let(:max_date) { Date.new(2020, 3) }
@@ -29,6 +197,7 @@ RSpec.describe Gitlab::Database::PartitioningMigrationHelpers::TableManagementHe
migration.create_table source_table do |t|
t.string :name, null: false
t.integer :age, null: false
+ t.integer partition_column2
t.datetime partition_column
t.datetime :updated_at
end
@@ -51,13 +220,15 @@ RSpec.describe Gitlab::Database::PartitioningMigrationHelpers::TableManagementHe
end
it 'delegates to a method on List::ConvertTable' do
- expect_next_instance_of(Gitlab::Database::Partitioning::List::ConvertTable,
- migration_context: migration,
- table_name: source_table,
- parent_table_name: partitioned_table,
- partitioning_column: partition_column,
- zero_partition_value: min_date,
- **extra_options) do |converter|
+ expect_next_instance_of(
+ Gitlab::Database::Partitioning::List::ConvertTable,
+ migration_context: migration,
+ table_name: source_table,
+ parent_table_name: partitioned_table,
+ partitioning_column: partition_column,
+ zero_partition_value: min_date,
+ **extra_options
+ ) do |converter|
expect(converter).to receive(expected_method)
end
@@ -70,11 +241,13 @@ RSpec.describe Gitlab::Database::PartitioningMigrationHelpers::TableManagementHe
let(:lock_tables) { [source_table] }
let(:expected_method) { :partition }
let(:migrate) do
- migration.convert_table_to_first_list_partition(table_name: source_table,
- partitioning_column: partition_column,
- parent_table_name: partitioned_table,
- initial_partitioning_value: min_date,
- lock_tables: lock_tables)
+ migration.convert_table_to_first_list_partition(
+ table_name: source_table,
+ partitioning_column: partition_column,
+ parent_table_name: partitioned_table,
+ initial_partitioning_value: min_date,
+ lock_tables: lock_tables
+ )
end
end
end
@@ -83,10 +256,12 @@ RSpec.describe Gitlab::Database::PartitioningMigrationHelpers::TableManagementHe
it_behaves_like 'delegates to ConvertTable' do
let(:expected_method) { :revert_partitioning }
let(:migrate) do
- migration.revert_converting_table_to_first_list_partition(table_name: source_table,
- partitioning_column: partition_column,
- parent_table_name: partitioned_table,
- initial_partitioning_value: min_date)
+ migration.revert_converting_table_to_first_list_partition(
+ table_name: source_table,
+ partitioning_column: partition_column,
+ parent_table_name: partitioned_table,
+ initial_partitioning_value: min_date
+ )
end
end
end
@@ -95,11 +270,13 @@ RSpec.describe Gitlab::Database::PartitioningMigrationHelpers::TableManagementHe
it_behaves_like 'delegates to ConvertTable' do
let(:expected_method) { :prepare_for_partitioning }
let(:migrate) do
- migration.prepare_constraint_for_list_partitioning(table_name: source_table,
- partitioning_column: partition_column,
- parent_table_name: partitioned_table,
- initial_partitioning_value: min_date,
- async: false)
+ migration.prepare_constraint_for_list_partitioning(
+ table_name: source_table,
+ partitioning_column: partition_column,
+ parent_table_name: partitioned_table,
+ initial_partitioning_value: min_date,
+ async: false
+ )
end
end
end
@@ -108,41 +285,200 @@ RSpec.describe Gitlab::Database::PartitioningMigrationHelpers::TableManagementHe
it_behaves_like 'delegates to ConvertTable' do
let(:expected_method) { :revert_preparation_for_partitioning }
let(:migrate) do
- migration.revert_preparing_constraint_for_list_partitioning(table_name: source_table,
- partitioning_column: partition_column,
- parent_table_name: partitioned_table,
- initial_partitioning_value: min_date)
+ migration.revert_preparing_constraint_for_list_partitioning(
+ table_name: source_table,
+ partitioning_column: partition_column,
+ parent_table_name: partitioned_table,
+ initial_partitioning_value: min_date
+ )
end
end
end
end
- describe '#partition_table_by_date' do
- let(:partition_column) { 'created_at' }
+ describe '#partition_table_by_int_range' do
let(:old_primary_key) { 'id' }
- let(:new_primary_key) { [old_primary_key, partition_column] }
+ let(:new_primary_key) { ['id', partition_column2] }
+ let(:partition_column_name) { partition_column2 }
+ let(:partitioned_model) { Class.new(ActiveRecord::Base) }
+ let(:timestamp) { Time.utc(2019, 12, 1, 12).round }
+ let(:partition_size) { 500 }
- context 'when the table is not allowed' do
- let(:source_table) { :_test_this_table_is_not_allowed }
+ subject { migration.partition_table_by_int_range(source_table, partition_column2, partition_size: partition_size, primary_key: ['id', partition_column2]) }
- it 'raises an error' do
- expect(migration).to receive(:assert_table_is_allowed).with(source_table).and_call_original
+ include_examples "a measurable object"
- expect do
- migration.partition_table_by_date source_table, partition_column, min_date: min_date, max_date: max_date
- end.to raise_error(/#{source_table} is not allowed for use/)
+ context 'simulates the merge_request_diff_commits migration' do
+ let(:table_name) { '_test_merge_request_diff_commits' }
+ let(:partition_column_name) { 'relative_order' }
+ let(:partition_size) { 2 }
+ let(:partitions) do
+ {
+ '1' => %w[1 3],
+ '3' => %w[3 5],
+ '5' => %w[5 7],
+ '7' => %w[7 9],
+ '9' => %w[9 11],
+ '11' => %w[11 13]
+ }
+ end
+
+ let(:buffer_partitions) do
+ {
+ '13' => %w[13 15],
+ '15' => %w[15 17],
+ '17' => %w[17 19],
+ '19' => %w[19 21],
+ '21' => %w[21 23],
+ '23' => %w[23 25]
+ }
+ end
+
+ let(:new_table_defition) do
+ {
+ new_path: { default: 'test', null: true, sql_type: 'text' },
+ merge_request_diff_id: { default: nil, null: false, sql_type: 'bigint' },
+ relative_order: { default: nil, null: false, sql_type: 'integer' }
+ }
+ end
+
+ let(:primary_key) { %w[merge_request_diff_id relative_order] }
+
+ before do
+ migration.create_table table_name, primary_key: primary_key do |t|
+ t.integer :merge_request_diff_id, null: false, default: 1
+ t.integer :relative_order, null: false
+ t.text :new_path, null: true, default: 'test'
+ end
+
+ source_model.table_name = table_name
+ end
+
+ it 'creates the partitions' do
+ migration.partition_table_by_int_range(table_name, partition_column_name, partition_size: partition_size, primary_key: primary_key)
+
+ expect_range_partitions_for(partitioned_table, partitions.merge(buffer_partitions))
+ end
+
+ it 'creates a composite primary key' do
+ migration.partition_table_by_int_range(table_name, partition_column_name, partition_size: partition_size, primary_key: primary_key)
+
+ expect(connection.primary_key(:_test_migration_partitioned_table)).to eql(%w[merge_request_diff_id relative_order])
+ end
+
+ it 'applies the correct column schema for the new table' do
+ migration.partition_table_by_int_range(table_name, partition_column_name, partition_size: partition_size, primary_key: primary_key)
+
+ columns = connection.columns(:_test_migration_partitioned_table)
+
+ columns.each do |column|
+ column_name = column.name.to_sym
+
+ expect(column.default).to eql(new_table_defition[column_name][:default])
+ expect(column.null).to eql(new_table_defition[column_name][:null])
+ expect(column.sql_type).to eql(new_table_defition[column_name][:sql_type])
+ end
+ end
+
+ it 'creates multiple partitions' do
+ migration.partition_table_by_int_range(table_name, partition_column_name, partition_size: 500, primary_key: primary_key)
+
+ expect_range_partitions_for(partitioned_table, {
+ '1' => %w[1 501],
+ '501' => %w[501 1001],
+ '1001' => %w[1001 1501],
+ '1501' => %w[1501 2001],
+ '2001' => %w[2001 2501],
+ '2501' => %w[2501 3001],
+ '3001' => %w[3001 3501],
+ '3501' => %w[3501 4001],
+ '4001' => %w[4001 4501],
+ '4501' => %w[4501 5001],
+ '5001' => %w[5001 5501],
+ '5501' => %w[5501 6001]
+ })
+ end
+
+ context 'when the table is not empty' do
+ before do
+ source_model.create!(merge_request_diff_id: 1, relative_order: 7, new_path: 'new_path')
+ end
+
+ let(:partition_size) { 2 }
+
+ let(:partitions) do
+ {
+ '1' => %w[1 3],
+ '3' => %w[3 5],
+ '5' => %w[5 7]
+ }
+ end
+
+ let(:buffer_partitions) do
+ {
+ '7' => %w[7 9],
+ '9' => %w[9 11],
+ '11' => %w[11 13],
+ '13' => %w[13 15],
+ '15' => %w[15 17],
+ '17' => %w[17 19]
+ }
+ end
+
+ it 'defaults the min_id to 1 and the max_id to 7' do
+ migration.partition_table_by_int_range(table_name, partition_column_name, partition_size: partition_size, primary_key: primary_key)
+
+ expect_range_partitions_for(partitioned_table, partitions.merge(buffer_partitions))
+ end
end
end
- context 'when run inside a transaction block' do
+ context 'when an invalid partition column is given' do
+ let(:invalid_column) { :_this_is_not_real }
+
it 'raises an error' do
- expect(migration).to receive(:transaction_open?).and_return(true)
+ expect do
+ migration.partition_table_by_int_range(source_table, invalid_column, partition_size: partition_size, primary_key: ['id'])
+ end.to raise_error(/partition column #{invalid_column} does not exist/)
+ end
+ end
+ context 'when partition_size is less than 1' do
+ let(:partition_size) { 1 }
+
+ it 'raises an error' do
expect do
- migration.partition_table_by_date source_table, partition_column, min_date: min_date, max_date: max_date
- end.to raise_error(/can not be run inside a transaction/)
+ subject
+ end.to raise_error(/partition_size must be greater than 1/)
+ end
+ end
+
+ context 'when the partitioned table already exists' do
+ before do
+ migration.send(:create_range_id_partitioned_copy, source_table,
+ migration.send(:make_partitioned_table_name, source_table),
+ connection.columns(source_table).find { |c| c.name == partition_column2 },
+ connection.columns(source_table).select { |c| new_primary_key.include?(c.name) })
+ end
+
+ it 'raises an error' do
+ expect(Gitlab::AppLogger).to receive(:warn).with(/Partitioned table not created because it already exists/)
+ expect { subject }.not_to raise_error
end
end
+ end
+
+ describe '#partition_table_by_date' do
+ let(:partition_column) { 'created_at' }
+ let(:old_primary_key) { 'id' }
+ let(:new_primary_key) { [old_primary_key, partition_column] }
+ let(:partition_column_name) { 'created_at' }
+ let(:partitioned_model) { Class.new(ActiveRecord::Base) }
+ let(:timestamp) { Time.utc(2019, 12, 1, 12).round }
+
+ subject { migration.partition_table_by_date source_table, partition_column, min_date: min_date, max_date: max_date }
+
+ include_examples "a measurable object"
context 'when the the max_date is less than the min_date' do
let(:max_date) { Time.utc(2019, 6) }
@@ -164,19 +500,6 @@ RSpec.describe Gitlab::Database::PartitioningMigrationHelpers::TableManagementHe
end
end
- context 'when the given table does not have a primary key' do
- it 'raises an error' do
- migration.execute(<<~SQL)
- ALTER TABLE #{source_table}
- DROP CONSTRAINT #{source_table}_pkey
- SQL
-
- expect do
- migration.partition_table_by_date source_table, partition_column, min_date: min_date, max_date: max_date
- end.to raise_error(/primary key not defined for #{source_table}/)
- end
- end
-
context 'when an invalid partition column is given' do
let(:invalid_column) { :_this_is_not_real }
@@ -188,34 +511,6 @@ RSpec.describe Gitlab::Database::PartitioningMigrationHelpers::TableManagementHe
end
describe 'constructing the partitioned table' do
- it 'creates a table partitioned by the proper column' do
- migration.partition_table_by_date source_table, partition_column, min_date: min_date, max_date: max_date
-
- expect(connection.table_exists?(partitioned_table)).to be(true)
- expect(connection.primary_key(partitioned_table)).to eq(new_primary_key)
-
- expect_table_partitioned_by(partitioned_table, [partition_column])
- end
-
- it 'requires the migration helper to be run in DDL mode' do
- expect(Gitlab::Database::QueryAnalyzers::RestrictAllowedSchemas).to receive(:require_ddl_mode!)
-
- migration.partition_table_by_date source_table, partition_column, min_date: min_date, max_date: max_date
-
- expect(connection.table_exists?(partitioned_table)).to be(true)
- expect(connection.primary_key(partitioned_table)).to eq(new_primary_key)
-
- expect_table_partitioned_by(partitioned_table, [partition_column])
- end
-
- it 'changes the primary key datatype to bigint' do
- migration.partition_table_by_date source_table, partition_column, min_date: min_date, max_date: max_date
-
- pk_column = connection.columns(partitioned_table).find { |c| c.name == old_primary_key }
-
- expect(pk_column.sql_type).to eq('bigint')
- end
-
context 'with a non-integer primary key datatype' do
before do
connection.create_table non_int_table, id: false do |t|
@@ -238,23 +533,6 @@ RSpec.describe Gitlab::Database::PartitioningMigrationHelpers::TableManagementHe
end
end
- it 'removes the default from the primary key column' do
- migration.partition_table_by_date source_table, partition_column, min_date: min_date, max_date: max_date
-
- pk_column = connection.columns(partitioned_table).find { |c| c.name == old_primary_key }
-
- expect(pk_column.default_function).to be_nil
- end
-
- it 'creates the partitioned table with the same non-key columns' do
- migration.partition_table_by_date source_table, partition_column, min_date: min_date, max_date: max_date
-
- copied_columns = filter_columns_by_name(connection.columns(partitioned_table), new_primary_key)
- original_columns = filter_columns_by_name(connection.columns(source_table), new_primary_key)
-
- expect(copied_columns).to match_array(original_columns)
- end
-
it 'creates a partition spanning over each month in the range given' do
migration.partition_table_by_date source_table, partition_column, min_date: min_date, max_date: max_date
@@ -340,75 +618,6 @@ RSpec.describe Gitlab::Database::PartitioningMigrationHelpers::TableManagementHe
end
end
end
-
- describe 'keeping data in sync with the partitioned table' do
- let(:partitioned_model) { Class.new(ActiveRecord::Base) }
- let(:timestamp) { Time.utc(2019, 12, 1, 12).round }
-
- before do
- partitioned_model.primary_key = :id
- partitioned_model.table_name = partitioned_table
- end
-
- it 'creates a trigger function on the original table' do
- expect_function_not_to_exist(function_name)
- expect_trigger_not_to_exist(source_table, trigger_name)
-
- migration.partition_table_by_date source_table, partition_column, min_date: min_date, max_date: max_date
-
- expect_function_to_exist(function_name)
- expect_valid_function_trigger(source_table, trigger_name, function_name, after: %w[delete insert update])
- end
-
- it 'syncs inserts to the partitioned tables' do
- migration.partition_table_by_date source_table, partition_column, min_date: min_date, max_date: max_date
-
- expect(partitioned_model.count).to eq(0)
-
- first_record = source_model.create!(name: 'Bob', age: 20, created_at: timestamp, updated_at: timestamp)
- second_record = source_model.create!(name: 'Alice', age: 30, created_at: timestamp, updated_at: timestamp)
-
- expect(partitioned_model.count).to eq(2)
- expect(partitioned_model.find(first_record.id).attributes).to eq(first_record.attributes)
- expect(partitioned_model.find(second_record.id).attributes).to eq(second_record.attributes)
- end
-
- it 'syncs updates to the partitioned tables' do
- migration.partition_table_by_date source_table, partition_column, min_date: min_date, max_date: max_date
-
- first_record = source_model.create!(name: 'Bob', age: 20, created_at: timestamp, updated_at: timestamp)
- second_record = source_model.create!(name: 'Alice', age: 30, created_at: timestamp, updated_at: timestamp)
-
- expect(partitioned_model.count).to eq(2)
-
- first_copy = partitioned_model.find(first_record.id)
- second_copy = partitioned_model.find(second_record.id)
-
- expect(first_copy.attributes).to eq(first_record.attributes)
- expect(second_copy.attributes).to eq(second_record.attributes)
-
- first_record.update!(age: 21, updated_at: timestamp + 1.hour)
-
- expect(partitioned_model.count).to eq(2)
- expect(first_copy.reload.attributes).to eq(first_record.attributes)
- expect(second_copy.reload.attributes).to eq(second_record.attributes)
- end
-
- it 'syncs deletes to the partitioned tables' do
- migration.partition_table_by_date source_table, partition_column, min_date: min_date, max_date: max_date
-
- first_record = source_model.create!(name: 'Bob', age: 20, created_at: timestamp, updated_at: timestamp)
- second_record = source_model.create!(name: 'Alice', age: 30, created_at: timestamp, updated_at: timestamp)
-
- expect(partitioned_model.count).to eq(2)
-
- first_record.destroy!
-
- expect(partitioned_model.count).to eq(1)
- expect(partitioned_model.find_by_id(first_record.id)).to be_nil
- expect(partitioned_model.find(second_record.id).attributes).to eq(second_record.attributes)
- end
- end
end
describe '#drop_partitioned_table_for' do
diff --git a/spec/lib/gitlab/database/postgres_constraint_spec.rb b/spec/lib/gitlab/database/postgres_constraint_spec.rb
index 75084a69115..140180540e0 100644
--- a/spec/lib/gitlab/database/postgres_constraint_spec.rb
+++ b/spec/lib/gitlab/database/postgres_constraint_spec.rb
@@ -51,9 +51,11 @@ RSpec.describe Gitlab::Database::PostgresConstraint, type: :model do
subject(:check_constraints) { described_class.check_constraints.by_table_identifier(table_identifier) }
it 'finds check constraints for the table' do
- expect(check_constraints.map(&:name)).to contain_exactly(check_constraint_a_positive,
- check_constraint_a_gt_b,
- invalid_constraint_a)
+ expect(check_constraints.map(&:name)).to contain_exactly(
+ check_constraint_a_positive,
+ check_constraint_a_gt_b,
+ invalid_constraint_a
+ )
end
it 'includes columns for the check constraints', :aggregate_failures do
@@ -108,8 +110,12 @@ RSpec.describe Gitlab::Database::PostgresConstraint, type: :model do
describe '#including_column' do
it 'only matches constraints on the given column' do
constraints_on_a = described_class.by_table_identifier(table_identifier).including_column('a').map(&:name)
- expect(constraints_on_a).to contain_exactly(check_constraint_a_positive, check_constraint_a_gt_b,
- unique_constraint_a, invalid_constraint_a)
+ expect(constraints_on_a).to contain_exactly(
+ check_constraint_a_positive,
+ check_constraint_a_gt_b,
+ unique_constraint_a,
+ invalid_constraint_a
+ )
end
end
diff --git a/spec/lib/gitlab/database/postgres_index_spec.rb b/spec/lib/gitlab/database/postgres_index_spec.rb
index d8a2612caf3..2e654a33a58 100644
--- a/spec/lib/gitlab/database/postgres_index_spec.rb
+++ b/spec/lib/gitlab/database/postgres_index_spec.rb
@@ -40,7 +40,7 @@ RSpec.describe Gitlab::Database::PostgresIndex do
it 'only btree and gist indexes' do
types = described_class.reindexing_support.map(&:type).uniq
- expect(types & %w(btree gist)).to eq(types)
+ expect(types & %w[btree gist]).to eq(types)
end
context 'with leftover indexes' do
@@ -71,7 +71,7 @@ RSpec.describe Gitlab::Database::PostgresIndex do
end
it 'retrieves leftover indexes matching the /_ccnew[0-9]*$/ pattern' do
- expect(subject.map(&:name)).to eq(%w(foobar_ccnew foobar_ccnew1))
+ expect(subject.map(&:name)).to eq(%w[foobar_ccnew foobar_ccnew1])
end
end
diff --git a/spec/lib/gitlab/database/query_analyzers/gitlab_schemas_metrics_spec.rb b/spec/lib/gitlab/database/query_analyzers/gitlab_schemas_metrics_spec.rb
index f325060e592..1909e134e66 100644
--- a/spec/lib/gitlab/database/query_analyzers/gitlab_schemas_metrics_spec.rb
+++ b/spec/lib/gitlab/database/query_analyzers/gitlab_schemas_metrics_spec.rb
@@ -35,7 +35,7 @@ RSpec.describe Gitlab::Database::QueryAnalyzers::GitlabSchemasMetrics, query_ana
},
"for query accessing gitlab_ci and gitlab_main" => {
model: ApplicationRecord,
- sql: "SELECT 1 FROM projects LEFT JOIN ci_builds ON ci_builds.project_id=projects.id",
+ sql: "SELECT 1 FROM projects LEFT JOIN p_ci_builds ON p_ci_builds.project_id=projects.id",
expectations: {
gitlab_schemas: "gitlab_ci,gitlab_main_cell",
db_config_name: "main"
@@ -43,7 +43,7 @@ RSpec.describe Gitlab::Database::QueryAnalyzers::GitlabSchemasMetrics, query_ana
},
"for query accessing gitlab_ci and gitlab_main the gitlab_schemas is always ordered" => {
model: ApplicationRecord,
- sql: "SELECT 1 FROM ci_builds LEFT JOIN projects ON ci_builds.project_id=projects.id",
+ sql: "SELECT 1 FROM p_ci_builds LEFT JOIN projects ON p_ci_builds.project_id=projects.id",
expectations: {
gitlab_schemas: "gitlab_ci,gitlab_main_cell",
db_config_name: "main"
@@ -51,7 +51,7 @@ RSpec.describe Gitlab::Database::QueryAnalyzers::GitlabSchemasMetrics, query_ana
},
"for query accessing CI database" => {
model: Ci::ApplicationRecord,
- sql: "SELECT 1 FROM ci_builds",
+ sql: "SELECT 1 FROM p_ci_builds",
expectations: {
gitlab_schemas: "gitlab_ci",
db_config_name: "ci"
diff --git a/spec/lib/gitlab/database/query_analyzers/gitlab_schemas_validate_connection_spec.rb b/spec/lib/gitlab/database/query_analyzers/gitlab_schemas_validate_connection_spec.rb
index e3ff5ab4779..0664508fa8d 100644
--- a/spec/lib/gitlab/database/query_analyzers/gitlab_schemas_validate_connection_spec.rb
+++ b/spec/lib/gitlab/database/query_analyzers/gitlab_schemas_validate_connection_spec.rb
@@ -26,14 +26,14 @@ RSpec.describe Gitlab::Database::QueryAnalyzers::GitlabSchemasValidateConnection
},
"for query accessing gitlab_ci and gitlab_main" => {
model: ApplicationRecord,
- sql: "SELECT 1 FROM projects LEFT JOIN ci_builds ON ci_builds.project_id=projects.id",
- expect_error: /The query tried to access \["projects", "ci_builds"\]/,
+ sql: "SELECT 1 FROM projects LEFT JOIN p_ci_builds ON p_ci_builds.project_id=projects.id",
+ expect_error: /The query tried to access \["projects", "p_ci_builds"\]/,
setup: -> (_) { skip_if_shared_database(:ci) }
},
"for query accessing gitlab_ci and gitlab_main the gitlab_schemas is always ordered" => {
model: ApplicationRecord,
- sql: "SELECT 1 FROM ci_builds LEFT JOIN projects ON ci_builds.project_id=projects.id",
- expect_error: /The query tried to access \["ci_builds", "projects"\]/,
+ sql: "SELECT 1 FROM p_ci_builds LEFT JOIN projects ON p_ci_builds.project_id=projects.id",
+ expect_error: /The query tried to access \["p_ci_builds", "projects"\]/,
setup: -> (_) { skip_if_shared_database(:ci) }
},
"for query accessing main table from CI database" => {
@@ -44,13 +44,13 @@ RSpec.describe Gitlab::Database::QueryAnalyzers::GitlabSchemasValidateConnection
},
"for query accessing CI database" => {
model: Ci::ApplicationRecord,
- sql: "SELECT 1 FROM ci_builds",
+ sql: "SELECT 1 FROM p_ci_builds",
expect_error: nil
},
"for query accessing CI table from main database" => {
model: ::ApplicationRecord,
- sql: "SELECT 1 FROM ci_builds",
- expect_error: /The query tried to access \["ci_builds"\]/,
+ sql: "SELECT 1 FROM p_ci_builds",
+ expect_error: /The query tried to access \["p_ci_builds"\]/,
setup: -> (_) { skip_if_shared_database(:ci) }
},
"for query accessing unknown gitlab_schema" => {
@@ -89,7 +89,7 @@ RSpec.describe Gitlab::Database::QueryAnalyzers::GitlabSchemasValidateConnection
it "throws an error when trying to access a table that belongs to the gitlab_ci schema from the main database" do
expect do
- ApplicationRecord.connection.execute("select * from ci_builds limit 1")
+ ApplicationRecord.connection.execute("select * from p_ci_builds limit 1")
end.to raise_error(Gitlab::Database::QueryAnalyzers::GitlabSchemasValidateConnection::CrossSchemaAccessError)
end
end
diff --git a/spec/lib/gitlab/database/query_analyzers/prevent_set_operator_mismatch/columns_spec.rb b/spec/lib/gitlab/database/query_analyzers/prevent_set_operator_mismatch/columns_spec.rb
new file mode 100644
index 00000000000..042bc297520
--- /dev/null
+++ b/spec/lib/gitlab/database/query_analyzers/prevent_set_operator_mismatch/columns_spec.rb
@@ -0,0 +1,88 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Database::QueryAnalyzers::PreventSetOperatorMismatch::Columns,
+ feature_category: :cell do
+ include PreventSetOperatorMismatchHelper
+
+ let_it_be(:namespace_columns) { Namespace.column_names.join(',') }
+
+ describe '.types' do
+ let(:node) { sql_select_node(sql) }
+ let(:cte_refs) { {} }
+ let(:select_stmt) do
+ Gitlab::Database::QueryAnalyzers::PreventSetOperatorMismatch::SelectStmt.new(node, cte_refs)
+ end
+
+ subject { described_class.types(select_stmt) }
+
+ context 'when static column' do
+ let(:sql) { 'SELECT id FROM namespaces' }
+
+ it do
+ expect(subject).to contain_exactly(Type::STATIC)
+ end
+
+ context 'with dynamic reference' do
+ let(:sql) { 'SELECT id FROM (SELECT * FROM namespaces) AS xyz' }
+
+ it do
+ expect(subject).to contain_exactly(Type::STATIC)
+ end
+ end
+ end
+
+ context 'when dynamic column' do
+ let(:sql) { 'SELECT * FROM namespaces' }
+
+ it do
+ expect(subject).to contain_exactly(Type::DYNAMIC)
+ end
+
+ context 'with static reference' do
+ let(:sql) { 'SELECT * FROM (SELECT 1) AS xyz' }
+
+ it do
+ expect(subject).to contain_exactly(Type::STATIC)
+ end
+ end
+ end
+
+ context 'when reference has errors' do
+ let(:cte_refs) { { 'namespaces' => [Type::INVALID].to_set } }
+ let(:sql) { 'SELECT * FROM namespaces' }
+
+ it 'forward through error state' do
+ expect(subject).to include(Type::INVALID)
+ end
+ end
+
+ context 'when static and dynamic columns' do
+ let(:sql) { 'SELECT *, users.id FROM namespaces, users' }
+
+ it do
+ expect(subject).to contain_exactly(Type::DYNAMIC, Type::STATIC)
+ end
+ end
+
+ context 'when static column and error' do
+ let(:error_column) { "SELECT #{namespace_columns} FROM namespaces UNION SELECT * FROM namespaces" }
+ let(:sql) { "SELECT id, (#{error_column}) FROM namespaces" }
+
+ it do
+ expect(subject).to contain_exactly(Type::STATIC, Type::INVALID)
+ end
+ end
+
+ context 'when dynamic column and error' do
+ let(:error_column) { "SELECT #{namespace_columns} FROM namespaces UNION SELECT * FROM namespaces" }
+ let(:sql) { "SELECT *, (#{error_column}) FROM namespaces" }
+
+ it do
+ # The sub-select is treated as a Type::STATIC column for now. This could do with some refinement.
+ expect(subject).to contain_exactly(Type::DYNAMIC, Type::STATIC, Type::INVALID)
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/database/query_analyzers/prevent_set_operator_mismatch/common_table_expressions_spec.rb b/spec/lib/gitlab/database/query_analyzers/prevent_set_operator_mismatch/common_table_expressions_spec.rb
new file mode 100644
index 00000000000..eacaa643ba5
--- /dev/null
+++ b/spec/lib/gitlab/database/query_analyzers/prevent_set_operator_mismatch/common_table_expressions_spec.rb
@@ -0,0 +1,97 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Database::QueryAnalyzers::PreventSetOperatorMismatch::CommonTableExpressions,
+ feature_category: :cell do
+ include PreventSetOperatorMismatchHelper
+
+ describe '.references' do
+ let(:node) { sql_select_node(sql) }
+ let(:cte_refs) { {} }
+
+ subject { described_class.references(node, cte_refs) }
+
+ context 'when standard CTE' do
+ let(:sql) do
+ <<-SQL
+ WITH some_cte AS (#{cte})
+ SELECT 1
+ FROM some_cte
+ SQL
+ end
+
+ context 'with static SELECT' do
+ let(:cte) { 'SELECT 1' }
+
+ it do
+ exp = { "some_cte" => Set.new([Type::STATIC]) }
+ expect(subject).to eq(exp)
+ end
+ end
+
+ context 'with dynamic SELECT' do
+ let(:cte) { 'SELECT * FROM namespaces' }
+
+ it do
+ exp = { "some_cte" => Set.new([Type::DYNAMIC]) }
+ expect(subject).to eq(exp)
+ end
+ end
+ end
+
+ context 'when recursive CTE' do
+ let(:sql) do
+ <<-SQL
+ WITH RECURSIVE some_cte AS (#{cte})
+ SELECT 1
+ FROM some_cte
+ SQL
+ end
+
+ context 'with static SELECT' do
+ let(:cte) { 'SELECT 1 UNION SELECT 2' }
+
+ it do
+ exp = { "some_cte" => Set.new([Type::STATIC]) }
+ expect(subject).to eq(exp)
+ end
+ end
+
+ context 'with dynamic SELECT' do
+ let(:cte) { 'SELECT * FROM namespaces UNION SELECT * FROM namespaces' }
+
+ it do
+ exp = { "some_cte" => Set.new([Type::DYNAMIC]) }
+ expect(subject).to eq(exp)
+ end
+ end
+
+ context 'with error SELECT' do
+ let(:cte) { 'SELECT * FROM namespaces UNION SELECT id FROM namespaces' }
+
+ it do
+ exp = { "some_cte" => Set.new([Type::DYNAMIC, Type::STATIC, Type::INVALID]) }
+ expect(subject).to eq(exp)
+ end
+ end
+ end
+
+ context 'with inherited CTE references' do
+ let(:sql) do
+ <<-SQL
+ WITH some_cte AS (SELECT 1)
+ SELECT 1
+ FROM some_cte
+ SQL
+ end
+
+ let(:cte_refs) { { 'some_reference' => 123 } }
+
+ it 'maintains inherited CTE references' do
+ subject_ref_names = subject.keys
+ expect(subject_ref_names).to eq(cte_refs.keys + ['some_cte'])
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/database/query_analyzers/prevent_set_operator_mismatch/froms_spec.rb b/spec/lib/gitlab/database/query_analyzers/prevent_set_operator_mismatch/froms_spec.rb
new file mode 100644
index 00000000000..03c0a845e60
--- /dev/null
+++ b/spec/lib/gitlab/database/query_analyzers/prevent_set_operator_mismatch/froms_spec.rb
@@ -0,0 +1,76 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Database::QueryAnalyzers::PreventSetOperatorMismatch::Froms,
+ feature_category: :cell do
+ include PreventSetOperatorMismatchHelper
+
+ describe '.references' do
+ let(:node) { sql_select_node(sql) }
+ let(:cte_refs) { {} }
+
+ subject { described_class.references(node, cte_refs) }
+
+ context 'when node is nil' do
+ let(:node) { nil }
+
+ it { is_expected.to eq({}) }
+ end
+
+ context 'when range_var' do
+ let(:sql) { 'SELECT 1 FROM namespaces' }
+
+ it { is_expected.to match({ 'namespaces' => an_instance_of(PgQuery::RangeVar) }) }
+ end
+
+ context 'when range_var with alias' do
+ let(:sql) { 'SELECT 1 FROM namespaces ns' }
+
+ it { is_expected.to match({ 'ns' => an_instance_of(PgQuery::RangeVar) }) }
+ end
+
+ context 'when join expression' do
+ let(:sql) do
+ <<-SQL
+ SELECT 1 FROM namespaces
+ INNER JOIN organizations ON namespaces.organization_id = organization.id
+ SQL
+ end
+
+ it do
+ is_expected.to match({
+ 'namespaces' => an_instance_of(PgQuery::RangeVar),
+ 'organizations' => an_instance_of(PgQuery::RangeVar)
+ })
+ end
+ end
+
+ context 'when join expression with alias' do
+ let(:sql) do
+ <<-SQL
+ SELECT 1 FROM namespaces ns
+ INNER JOIN organizations o ON ns.organization_id = o.id
+ SQL
+ end
+
+ it do
+ is_expected.to match({
+ 'ns' => an_instance_of(PgQuery::RangeVar),
+ 'o' => an_instance_of(PgQuery::RangeVar)
+ })
+ end
+ end
+
+ context 'when sub-query' do
+ let(:sql) do
+ <<-SQL
+ SELECT 1
+ FROM (SELECT 1) some_subquery
+ SQL
+ end
+
+ it { is_expected.to match({ 'some_subquery' => [Type::STATIC].to_set }) }
+ end
+ end
+end
diff --git a/spec/lib/gitlab/database/query_analyzers/prevent_set_operator_mismatch/node_spec.rb b/spec/lib/gitlab/database/query_analyzers/prevent_set_operator_mismatch/node_spec.rb
new file mode 100644
index 00000000000..a8294376107
--- /dev/null
+++ b/spec/lib/gitlab/database/query_analyzers/prevent_set_operator_mismatch/node_spec.rb
@@ -0,0 +1,68 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Database::QueryAnalyzers::PreventSetOperatorMismatch::Node, feature_category: :cell do
+ include PreventSetOperatorMismatchHelper
+
+ let(:sql) { 'SELECT id FROM namespaces' }
+ let(:node) { sql_select_node(sql) }
+
+ describe '.descendants' do
+ context 'with a block' do
+ it do
+ nodes = []
+ described_class.descendants(node.from_clause) do |node|
+ nodes << node.class
+ end
+ expect(nodes).to match_array [PgQuery::Node, PgQuery::RangeVar]
+ end
+ end
+
+ context 'without a block' do
+ subject { described_class.descendants(node) }
+
+ it { is_expected.to be_instance_of Enumerator }
+ end
+
+ context 'with a filter' do
+ let(:filter) { ->(field) { %i[from_clause target_list].include?(field) } }
+
+ subject { described_class.descendants(node, filter: filter).count }
+
+ it 'only traverse nodes that match the filter' do
+ is_expected.to eq 2
+ end
+ end
+ end
+
+ describe '.locate_descendant' do
+ subject { described_class.locate_descendant(node.target_list, :res_target) }
+
+ it { is_expected.to be_instance_of PgQuery::ResTarget }
+
+ context 'with a filter' do
+ subject { described_class.locate_descendant(node.target_list, :res_target, filter: ->(_) { false }) }
+
+ it { is_expected.to be_nil }
+ end
+ end
+
+ describe '.locate_descendants' do
+ subject { described_class.locate_descendants(node.target_list, :res_target) }
+
+ it { is_expected.to be_instance_of Array }
+
+ context 'with a filter' do
+ subject { described_class.locate_descendant(node.target_list, :res_target, filter: ->(_) { false }) }
+
+ it { is_expected.to be_nil }
+ end
+ end
+
+ describe '.dig' do
+ subject { described_class.dig(node.target_list[0], :res_target, :val, :column_ref) }
+
+ it { is_expected.to be_instance_of PgQuery::ColumnRef }
+ end
+end
diff --git a/spec/lib/gitlab/database/query_analyzers/prevent_set_operator_mismatch/references_spec.rb b/spec/lib/gitlab/database/query_analyzers/prevent_set_operator_mismatch/references_spec.rb
new file mode 100644
index 00000000000..0f0f92aa1f2
--- /dev/null
+++ b/spec/lib/gitlab/database/query_analyzers/prevent_set_operator_mismatch/references_spec.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Database::QueryAnalyzers::PreventSetOperatorMismatch::References,
+ feature_category: :cell do
+ include PreventSetOperatorMismatchHelper
+
+ let(:refs) do
+ {
+ 'resolved_reference' => Set.new,
+ 'unresolved_reference' => double,
+ 'table_reference' => PgQuery::RangeVar.new,
+ 'error_reference' => [Type::INVALID].to_set
+ }
+ end
+
+ describe '.resolved' do
+ subject { described_class.resolved(refs) }
+
+ it { is_expected.to eq refs.slice('resolved_reference', 'error_reference') }
+ end
+
+ describe '.unresolved' do
+ subject { described_class.unresolved(refs) }
+
+ it { is_expected.to eq refs.slice('unresolved_reference') }
+ end
+
+ describe '.errors?' do
+ subject { described_class.errors?(refs) }
+
+ it { is_expected.to be_truthy }
+
+ context 'when no errors exist' do
+ subject { described_class.errors?(refs.except('error_reference')) }
+
+ it { is_expected.to be_falsey }
+ end
+ end
+end
diff --git a/spec/lib/gitlab/database/query_analyzers/prevent_set_operator_mismatch/select_stmt_spec.rb b/spec/lib/gitlab/database/query_analyzers/prevent_set_operator_mismatch/select_stmt_spec.rb
new file mode 100644
index 00000000000..52d6c9f1032
--- /dev/null
+++ b/spec/lib/gitlab/database/query_analyzers/prevent_set_operator_mismatch/select_stmt_spec.rb
@@ -0,0 +1,361 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Database::QueryAnalyzers::PreventSetOperatorMismatch::SelectStmt, feature_category: :cell do
+ include PreventSetOperatorMismatchHelper
+
+ let_it_be(:static_namespace_columns) { Namespace.column_names.join(', ') }
+
+ let(:node) { sql_select_node(sql) }
+
+ subject { described_class.new(node).types }
+
+ shared_examples 'valid SQL' do
+ it { is_expected.not_to include(Type::INVALID) }
+ end
+
+ shared_examples 'invalid SQL' do
+ it { is_expected.to include(Type::INVALID) }
+ end
+
+ shared_context 'with basic set operator queries' do
+ let(:set_operator_queries) do
+ {
+ 'set operator with static columns' => <<-SQL,
+ SELECT id, name FROM namespaces WHERE name = 'test1'
+ #{set_operator}
+ SELECT id, name FROM namespaces WHERE name = 'test2'
+ SQL
+ 'set operator with static referenced columns' => <<-SQL,
+ SELECT namespaces.id, name FROM namespaces WHERE name = 'test1'
+ #{set_operator}
+ SELECT id, namespaces.name FROM namespaces WHERE name = 'test2'
+ SQL
+ 'set operator with static alias referenced columns' => <<-SQL,
+ SELECT namespaces.id, name FROM namespaces WHERE name = 'test1'
+ #{set_operator}
+ SELECT id, namespaces2.name FROM namespaces2 WHERE name = 'test2'
+ SQL
+ 'set operator with dynamic columns' => <<-SQL,
+ SELECT * FROM namespaces WHERE name = 'test1'
+ #{set_operator}
+ SELECT * FROM namespaces WHERE name = 'test2'
+ SQL
+ 'set operator with dynamic referenced columns' => <<-SQL,
+ SELECT namespaces.* FROM namespaces WHERE name = 'test1'
+ #{set_operator}
+ SELECT namespaces.* FROM namespaces WHERE name = 'test2'
+ SQL
+ 'set operator with dynamic referenced aliased columns' => <<-SQL,
+ SELECT namespaces.* FROM namespaces WHERE name = 'test1'
+ #{set_operator}
+ SELECT namespaces2.* FROM namespaces namespaces2 WHERE name = 'test2'
+ SQL
+ 'set operator with dynamic columns without using star' => <<-SQL,
+ SELECT namespaces FROM namespaces WHERE name = 'test1'
+ #{set_operator}
+ SELECT * FROM namespaces WHERE name = 'test2'
+ SQL
+ 'set operator with single dynamic referenced columns' => <<-SQL,
+ SELECT namespaces.* FROM namespaces WHERE name = 'test1'
+ #{set_operator}
+ SELECT * FROM namespaces WHERE name = 'test2'
+ SQL
+ 'set operator with static and dynamic columns' => <<-SQL,
+ SELECT #{static_namespace_columns} FROM namespaces WHERE name = 'test1'
+ #{set_operator}
+ SELECT * FROM namespaces WHERE name = 'test2'
+ SQL
+ 'set operator with static aliased columns and dynamic columns' => <<-SQL,
+ SELECT #{Namespace.column_names.map { |c| "namespaces2.#{c}" }.join(', ')}
+ FROM namespaces namespaces2
+ WHERE name = 'test1'
+ #{set_operator}
+ SELECT * FROM namespaces WHERE name = 'test2'
+ SQL
+ 'set operator with static columns and dynamic aliased columns' => <<-SQL,
+ SELECT #{static_namespace_columns} FROM namespaces WHERE name = 'test1'
+ #{set_operator}
+ SELECT namespaces2.* FROM namespaces namespaces2 WHERE name = 'test2'
+ SQL
+ 'set operator with static and dynamic aliased columns' => <<-SQL,
+ SELECT #{Namespace.column_names.map { |c| "namespaces2.#{c}" }.join(', ')}
+ FROM namespaces namespaces2
+ WHERE name = 'test1'
+ #{set_operator}
+ SELECT namespaces3.* FROM namespaces namespaces3 WHERE name = 'test2'
+ SQL
+ 'set operator with mixed dynamic and static columns' => <<-SQL,
+ SELECT namespaces.*, projects.id FROM namespaces, projects WHERE name = 'test1'
+ #{set_operator}
+ SELECT namespaces.*, projects.id FROM namespaces, projects WHERE name = 'test2'
+ SQL
+ 'set operator without references' => <<-SQL
+ SELECT 1
+ #{set_operator}
+ SELECT 2
+ SQL
+ }
+ end
+
+ where(:query_name, :behavior) do
+ [
+ ['set operator with static columns', 'valid SQL'],
+ ['set operator with static referenced columns', 'valid SQL'],
+ ['set operator with static alias referenced columns', 'valid SQL'],
+ ['set operator with dynamic columns', 'valid SQL'],
+ ['set operator with dynamic referenced columns', 'valid SQL'],
+ ['set operator with dynamic referenced aliased columns', 'valid SQL'],
+ ['set operator with dynamic columns without using star', 'invalid SQL'],
+ ['set operator with single dynamic referenced columns', 'valid SQL'],
+ ['set operator with static and dynamic columns', 'invalid SQL'],
+ ['set operator with static and dynamic aliased columns', 'invalid SQL'],
+ ['set operator with static aliased columns and dynamic columns', 'invalid SQL'],
+ ['set operator with static columns and dynamic aliased columns', 'invalid SQL'],
+ ['set operator with static and dynamic aliased columns', 'invalid SQL'],
+ ['set operator with mixed dynamic and static columns', 'valid SQL'],
+ ['set operator without references', 'valid SQL']
+ ]
+ end
+ end
+
+ %w[UNION INTERSECT EXCEPT].each do |set_operator|
+ context "with #{set_operator}" do
+ let(:set_operator) { set_operator }
+
+ context "for basic #{set_operator} queries" do
+ include_context 'with basic set operator queries'
+
+ with_them do
+ let(:sql) { set_operator_queries[query_name] }
+
+ it_behaves_like params[:behavior]
+ end
+ end
+
+ context 'for subquery' do
+ context "with #{set_operator}" do
+ where(:select_columns) do
+ [
+ ['*'],
+ ['sub.*'],
+ ['sub'],
+ ['sub.id']
+ ]
+ end
+
+ with_them do
+ include_context 'with basic set operator queries'
+
+ with_them do
+ let(:sql) do
+ <<-SQL
+ SELECT #{select_columns}
+ FROM (
+ #{set_operator_queries[query_name]}
+ ) sub
+ SQL
+ end
+
+ it_behaves_like params[:behavior]
+ end
+ end
+ end
+
+ context "when used by one side of #{set_operator}" do
+ let(:sql) do
+ <<-SQL
+ SELECT #{union1}
+ FROM (
+ SELECT #{subquery}
+ FROM namespaces
+ ) namespaces
+
+ #{set_operator}
+
+ SELECT #{union2}
+ FROM namespaces
+ SQL
+ end
+
+ where(:union1, :union2, :subquery, :expected) do
+ [
+ ['*', '*', '*', 'valid SQL'],
+ [ref(:static_namespace_columns), '*', '*', 'invalid SQL'],
+ ['*', ref(:static_namespace_columns), '*', 'invalid SQL'],
+ ['*', '*', ref(:static_namespace_columns), 'invalid SQL'],
+ [ref(:static_namespace_columns), ref(:static_namespace_columns), '*', 'valid SQL'],
+ [ref(:static_namespace_columns), '*', ref(:static_namespace_columns), 'invalid SQL'],
+ ['*', ref(:static_namespace_columns), ref(:static_namespace_columns), 'valid SQL'],
+ ['namespaces', 'namespaces', 'namespaces', 'valid SQL'],
+ # Used by our keyset pagination queries.
+ ['NULL :: namespaces', 'namespaces', 'id, name', 'valid SQL'],
+ ['NULL :: namespaces, id, name', 'namespaces, id, name', 'namespaces', 'valid SQL']
+ ]
+ end
+
+ with_them do
+ it_behaves_like params[:expected]
+ end
+ end
+ end
+
+ context 'for CTE' do
+ context "when #{set_operator}" do
+ where(:select_columns) do
+ [
+ ['*'],
+ ['namespaces_cte.*'],
+ ['namespaces_cte.id']
+ ]
+ end
+
+ with_them do
+ include_context 'with basic set operator queries'
+
+ with_them do
+ let(:sql) do
+ <<-SQL
+ WITH namespaces_cte AS (
+ #{set_operator_queries[query_name]}
+ )
+ SELECT *
+ FROM namespaces_cte
+ SQL
+ end
+
+ it_behaves_like params[:behavior]
+ end
+ end
+ end
+
+ context "when used by one side of #{set_operator}" do
+ let(:sql) do
+ <<-SQL
+ WITH #{cte_name} AS (
+ SELECT #{cte_select_columns}
+ FROM namespaces
+ )
+ SELECT #{select_columns}
+ FROM #{cte_name}
+
+ #{set_operator}
+
+ SELECT *
+ FROM namespaces
+ SQL
+ end
+
+ where(:cte_select_columns, :select_columns, :cte_name, :expected) do
+ [
+ ['*', '*', 'some_cte', 'valid SQL'],
+ [ref(:static_namespace_columns), '*', 'some_cte', 'invalid SQL'],
+ ['*', ref(:static_namespace_columns), 'some_cte', 'invalid SQL'],
+ [ref(:static_namespace_columns), ref(:static_namespace_columns), 'some_cte', 'invalid SQL'],
+ ['*', '*', 'some_cte', 'valid SQL'],
+ # Same scenarios as above, but the CTE name matches the table name in the CTE.
+ ['*', '*', 'namespaces', 'valid SQL'],
+ [ref(:static_namespace_columns), '*', 'namespaces', 'valid SQL'],
+ ['*', ref(:static_namespace_columns), 'namespaces', 'invalid SQL'],
+ [ref(:static_namespace_columns), ref(:static_namespace_columns), 'namespaces', 'valid SQL'],
+ ['*', '*', 'namespaces', 'valid SQL']
+ ]
+ end
+
+ with_them do
+ it_behaves_like params[:expected]
+ end
+ end
+
+ context 'when recursive' do
+ let(:sql) do
+ <<-SQL
+ WITH RECURSIVE namespaces_cte AS (
+ (
+ SELECT #{select1}
+ FROM namespaces
+ )
+ UNION
+ (
+ SELECT #{select2}
+ FROM namespaces_cte
+ )
+ )
+ SELECT *
+ FROM namespaces_cte
+ SQL
+ end
+
+ where(:select1, :select2, :expected) do
+ [
+ ['id', 'id', 'valid SQL'],
+ [ref(:static_namespace_columns), '*', 'valid SQL'],
+ ['*', ref(:static_namespace_columns), 'invalid SQL']
+ ]
+ end
+
+ with_them do
+ it_behaves_like params[:expected]
+ end
+ end
+ end
+
+ context 'for subselect' do
+ context 'with set operator' do
+ let(:sql) do
+ <<-SQL
+ SELECT (
+ SELECT id FROM namespaces
+ #{set_operator}
+ SELECT id FROM namespaces
+ ) AS namespace_id
+ SQL
+ end
+
+ it_behaves_like 'valid SQL'
+ end
+ end
+ end
+ end
+
+ context 'with lateral join' do
+ let(:sql) do
+ <<-SQL
+ SELECT namespaces.id
+ FROM
+ namespaces CROSS
+ JOIN LATERAL (
+ SELECT
+ namespaces.traversal_ids[array_length(namespaces.traversal_ids, 1) ] AS id
+ FROM
+ namespaces
+ WHERE
+ namespaces.type = 'Group'
+ AND namespaces.traversal_ids @ > ARRAY[members.source_id]
+ ) namespaces
+ SQL
+ end
+
+ pending
+ end
+
+ context 'when columns are not referenced' do
+ let(:sql) do
+ <<-SQL
+ SELECT
+ COUNT(1)
+ FROM (
+ SELECT #{static_namespace_columns}
+ FROM namespaces
+ UNION
+ SELECT *
+ FROM namespaces
+ ) invalid_union
+ SQL
+ end
+
+ # Error will bubble up even though the parent query does not reference any of the sub-query columns.
+ it_behaves_like 'invalid SQL'
+ end
+end
diff --git a/spec/lib/gitlab/database/query_analyzers/prevent_set_operator_mismatch/targets_spec.rb b/spec/lib/gitlab/database/query_analyzers/prevent_set_operator_mismatch/targets_spec.rb
new file mode 100644
index 00000000000..2ea69f3726e
--- /dev/null
+++ b/spec/lib/gitlab/database/query_analyzers/prevent_set_operator_mismatch/targets_spec.rb
@@ -0,0 +1,94 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Database::QueryAnalyzers::PreventSetOperatorMismatch::Targets, feature_category: :cell do
+ include PreventSetOperatorMismatchHelper
+
+ let(:node) { sql_select_node(sql) }
+ let(:select_stmt) { Gitlab::Database::QueryAnalyzers::PreventSetOperatorMismatch::SelectStmt.new(node) }
+ let(:target) { node.target_list[0].res_target }
+
+ describe '.reference_names' do
+ subject { described_class.reference_names(target, select_stmt) }
+
+ context 'with a literal target' do
+ let(:sql) { 'SELECT 1' }
+
+ it { is_expected.to be_empty }
+ end
+
+ context 'with a function target' do
+ let(:sql) { 'SELECT unnest(ARRAY[1,2]) FROM namespaces, users' }
+
+ it { is_expected.to be_empty }
+ end
+
+ context 'with a subselect target' do
+ let(:sql) { 'SELECT (SELECT 1) xyz FROM namespaces' }
+
+ it { is_expected.to eq(%w[xyz_subselect]) }
+
+ it 'updates all_references in the select statement' do
+ expect { subject }.to change { select_stmt.all_references }
+ .to include('xyz_subselect')
+ end
+ end
+
+ context 'with an unqualified column name' do
+ let(:sql) { 'SELECT id FROM namespaces, users' }
+
+ it { is_expected.to eq(%w[namespaces users]) }
+ end
+
+ context 'with a qualified column name' do
+ let(:sql) { 'SELECT namespaces.id FROM namespaces, users' }
+
+ it { is_expected.to eq(%w[namespaces]) }
+ end
+
+ context 'with a table name' do
+ let(:sql) { 'SELECT namespaces FROM namespaces, users' }
+
+ it { is_expected.to eq(%w[namespaces]) }
+ end
+
+ context 'with a *' do
+ let(:sql) { 'SELECT * FROM namespaces, users' }
+
+ it { is_expected.to eq(%w[namespaces users]) }
+ end
+ end
+
+ describe '.a_star?' do
+ subject { described_class.a_star?(target) }
+
+ context 'when * is used' do
+ let(:sql) { 'SELECT * FROM namespaces' }
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'when no * is used' do
+ let(:sql) { 'SELECT 1' }
+
+ it { is_expected.to be_falsey }
+ end
+ end
+
+ describe '.null?' do
+ subject { described_class.null?(target) }
+
+ context 'when target is null' do
+ let(:sql) { 'SELECT NULL::namespaces FROM namespaces' }
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'when target is not null' do
+ let(:sql) { 'SELECT 1' }
+
+ it { is_expected.to be_falsey }
+ end
+ end
+end
diff --git a/spec/lib/gitlab/database/query_analyzers/prevent_set_operator_mismatch_spec.rb b/spec/lib/gitlab/database/query_analyzers/prevent_set_operator_mismatch_spec.rb
new file mode 100644
index 00000000000..28c155c1eb1
--- /dev/null
+++ b/spec/lib/gitlab/database/query_analyzers/prevent_set_operator_mismatch_spec.rb
@@ -0,0 +1,84 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Database::QueryAnalyzers::PreventSetOperatorMismatch, query_analyzers: false, feature_category: :cell do
+ let(:analyzer) { described_class }
+ let_it_be(:static_namespace_columns) { Namespace.column_names.join(', ') }
+
+ def process_sql(sql, model = ApplicationRecord)
+ Gitlab::Database::QueryAnalyzer.instance.within([analyzer]) do
+ # Skip load balancer and retrieve connection assigned to model
+ Gitlab::Database::QueryAnalyzer.instance.send(:process_sql, sql, model.retrieve_connection)
+ end
+ end
+
+ shared_examples 'parses SQL' do
+ it do
+ expect_next_instance_of(described_class::SelectStmt) do |select_stmt|
+ expect(select_stmt).to receive(:types).and_return(Set.new)
+ end
+
+ process_sql sql
+ end
+ end
+
+ context 'when SQL includes a UNION' do
+ let(:sql) { 'SELECT 1 UNION SELECT 2' }
+
+ include_examples 'parses SQL'
+ end
+
+ context 'when SQL includes a INTERSECT' do
+ let(:sql) { 'SELECT 1 INTERSECT SELECT 2' }
+
+ include_examples 'parses SQL'
+ end
+
+ context 'when SQL includes a EXCEPT' do
+ let(:sql) { 'SELECT 1 EXCEPT SELECT 2' }
+
+ include_examples 'parses SQL'
+ end
+
+ context 'when SQL does not include a set operator' do
+ let(:sql) { 'SELECT 1' }
+
+ it 'does not parse SQL' do
+ expect(described_class::SelectStmt).not_to receive(:new)
+
+ process_sql sql
+ end
+ end
+
+ context 'when SQL is invalid' do
+ it 'raises error' do
+ expect do
+ process_sql "SELECT #{static_namespace_columns} FROM namespaces UNION SELECT * FROM namespaces"
+ end.to raise_error(described_class::SetOperatorStarError)
+ end
+ end
+
+ context 'when SQL is valid' do
+ it 'does not raise error' do
+ expect do
+ process_sql 'SELECT 1'
+ end.not_to raise_error
+ end
+ end
+
+ context 'when SQL has many select statements' do
+ let(:sql) do
+ <<-SQL
+ SELECT 1 UNION SELECT 1;
+ SELECT #{static_namespace_columns} FROM namespaces UNION SELECT * FROM namespaces
+ SQL
+ end
+
+ it 'raises error' do
+ expect do
+ process_sql sql
+ end.to raise_error(described_class::SetOperatorStarError)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/database/reindexing_spec.rb b/spec/lib/gitlab/database/reindexing_spec.rb
index 441f6476abe..2321f5d933d 100644
--- a/spec/lib/gitlab/database/reindexing_spec.rb
+++ b/spec/lib/gitlab/database/reindexing_spec.rb
@@ -231,7 +231,7 @@ RSpec.describe Gitlab::Database::Reindexing, feature_category: :database, time_t
states = queued_actions.map(&:reload).map(&:state)
- expect(states).to eq(%w(failed done queued))
+ expect(states).to eq(%w[failed done queued])
end
end
diff --git a/spec/lib/gitlab/database/tables_locker_spec.rb b/spec/lib/gitlab/database/tables_locker_spec.rb
index 0e7e929d54b..e3bd61b32a1 100644
--- a/spec/lib/gitlab/database/tables_locker_spec.rb
+++ b/spec/lib/gitlab/database/tables_locker_spec.rb
@@ -33,30 +33,40 @@ RSpec.describe Gitlab::Database::TablesLocker, :suppress_gitlab_schemas_validate
FOR VALUES IN (0)
SQL
- ApplicationRecord.connection.execute(create_partition_sql)
- Ci::ApplicationRecord.connection.execute(create_partition_sql)
-
create_detached_partition_sql = <<~SQL
CREATE TABLE IF NOT EXISTS #{Gitlab::Database::DYNAMIC_PARTITIONS_SCHEMA}._test_gitlab_main_part_202201 (
id bigserial primary key not null
)
SQL
- ApplicationRecord.connection.execute(create_detached_partition_sql)
- Ci::ApplicationRecord.connection.execute(create_detached_partition_sql)
+ [ApplicationRecord, Ci::ApplicationRecord]
+ .map(&:connection)
+ .each do |conn|
+ conn.execute(create_partition_sql)
+ conn.execute(
+ "DROP TABLE IF EXISTS #{Gitlab::Database::DYNAMIC_PARTITIONS_SCHEMA}._test_gitlab_main_part_202201"
+ )
+ conn.execute(create_detached_partition_sql)
+
+ Gitlab::Database::SharedModel.using_connection(conn) do
+ Postgresql::DetachedPartition.delete_all
+ Postgresql::DetachedPartition.create!(
+ table_name: '_test_gitlab_main_part_20220101',
+ drop_after: Time.current
+ )
+ end
+ end
+ end
- Gitlab::Database::SharedModel.using_connection(ApplicationRecord.connection) do
- Postgresql::DetachedPartition.create!(
- table_name: '_test_gitlab_main_part_20220101',
- drop_after: Time.current
- )
- end
- Gitlab::Database::SharedModel.using_connection(Ci::ApplicationRecord.connection) do
- Postgresql::DetachedPartition.create!(
- table_name: '_test_gitlab_main_part_20220101',
- drop_after: Time.current
- )
- end
+ after(:all) do
+ [ApplicationRecord, Ci::ApplicationRecord]
+ .map(&:connection)
+ .each do |conn|
+ conn.execute(
+ "DROP TABLE IF EXISTS #{Gitlab::Database::DYNAMIC_PARTITIONS_SCHEMA}._test_gitlab_main_part_202201"
+ )
+ Gitlab::Database::SharedModel.using_connection(conn) { Postgresql::DetachedPartition.delete_all }
+ end
end
shared_examples "lock tables" do |gitlab_schema, database_name|
diff --git a/spec/lib/gitlab/database/tables_truncate_spec.rb b/spec/lib/gitlab/database/tables_truncate_spec.rb
index e41c7d34378..352c2fff779 100644
--- a/spec/lib/gitlab/database/tables_truncate_spec.rb
+++ b/spec/lib/gitlab/database/tables_truncate_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe Gitlab::Database::TablesTruncate, :reestablished_active_record_base,
- :suppress_gitlab_schemas_validate_connection, feature_category: :cell do
+ :suppress_gitlab_schemas_validate_connection, feature_category: :cell do
include MigrationsHelpers
let(:min_batch_size) { 1 }
@@ -373,7 +373,9 @@ RSpec.describe Gitlab::Database::TablesTruncate, :reestablished_active_record_ba
context 'with no main data in ci datatabase' do
before do
# Remove 'main' data in ci database
- ci_connection.truncate_tables([:_test_gitlab_main_items, :_test_gitlab_main_references])
+ ci_connection.execute(
+ "TRUNCATE TABLE _test_gitlab_main_items, _test_gitlab_main_references RESTART IDENTITY CASCADE;"
+ )
end
it { is_expected.to eq(false) }
diff --git a/spec/lib/gitlab/database/transaction/observer_spec.rb b/spec/lib/gitlab/database/transaction/observer_spec.rb
index d1cb014a594..778212add66 100644
--- a/spec/lib/gitlab/database/transaction/observer_spec.rb
+++ b/spec/lib/gitlab/database/transaction/observer_spec.rb
@@ -25,7 +25,7 @@ RSpec.describe Gitlab::Database::Transaction::Observer, feature_category: :datab
User.first
expect(transaction_context).to be_a(::Gitlab::Database::Transaction::Context)
- expect(context.keys).to match_array(%i(start_time depth savepoints queries backtraces external_http_count_start external_http_duration_start))
+ expect(context.keys).to match_array(%i[start_time depth savepoints queries backtraces external_http_count_start external_http_duration_start])
expect(context[:depth]).to eq(2)
expect(context[:savepoints]).to eq(1)
expect(context[:queries].length).to eq(1)
diff --git a/spec/lib/gitlab/dependency_linker/base_linker_spec.rb b/spec/lib/gitlab/dependency_linker/base_linker_spec.rb
index 2811bc859da..ef61a962279 100644
--- a/spec/lib/gitlab/dependency_linker/base_linker_spec.rb
+++ b/spec/lib/gitlab/dependency_linker/base_linker_spec.rb
@@ -46,8 +46,8 @@ RSpec.describe Gitlab::DependencyLinker::BaseLinker do
'target="_blank"'
]
- attrs.unshift(%{href="#{url}"}) if url
+ attrs.unshift(%(href="#{url}")) if url
- %{<a #{attrs.join(' ')}>#{text}</a>}
+ %(<a #{attrs.join(' ')}>#{text}</a>)
end
end
diff --git a/spec/lib/gitlab/dependency_linker/cargo_toml_linker_spec.rb b/spec/lib/gitlab/dependency_linker/cargo_toml_linker_spec.rb
index 7f6b3b86799..d147afbf3bd 100644
--- a/spec/lib/gitlab/dependency_linker/cargo_toml_linker_spec.rb
+++ b/spec/lib/gitlab/dependency_linker/cargo_toml_linker_spec.rb
@@ -70,7 +70,7 @@ RSpec.describe Gitlab::DependencyLinker::CargoTomlLinker do
subject { Gitlab::Highlight.highlight(file_name, file_content) }
def link(name, url)
- %{<a href="#{url}" rel="nofollow noreferrer noopener" target="_blank">#{name}</a>}
+ %(<a href="#{url}" rel="nofollow noreferrer noopener" target="_blank">#{name}</a>)
end
it 'links dependencies' do
diff --git a/spec/lib/gitlab/dependency_linker/cartfile_linker_spec.rb b/spec/lib/gitlab/dependency_linker/cartfile_linker_spec.rb
index 52ddba24458..eeb7471cd18 100644
--- a/spec/lib/gitlab/dependency_linker/cartfile_linker_spec.rb
+++ b/spec/lib/gitlab/dependency_linker/cartfile_linker_spec.rb
@@ -54,7 +54,7 @@ RSpec.describe Gitlab::DependencyLinker::CartfileLinker do
subject { Gitlab::Highlight.highlight(file_name, file_content) }
def link(name, url)
- %{<a href="#{url}" rel="nofollow noreferrer noopener" target="_blank">#{name}</a>}
+ %(<a href="#{url}" rel="nofollow noreferrer noopener" target="_blank">#{name}</a>)
end
it 'links dependencies' do
diff --git a/spec/lib/gitlab/dependency_linker/composer_json_linker_spec.rb b/spec/lib/gitlab/dependency_linker/composer_json_linker_spec.rb
index 02fac96a02f..cf636a7f201 100644
--- a/spec/lib/gitlab/dependency_linker/composer_json_linker_spec.rb
+++ b/spec/lib/gitlab/dependency_linker/composer_json_linker_spec.rb
@@ -50,7 +50,7 @@ RSpec.describe Gitlab::DependencyLinker::ComposerJsonLinker do
subject { Gitlab::Highlight.highlight(file_name, file_content) }
def link(name, url)
- %{<a href="#{url}" rel="nofollow noreferrer noopener" target="_blank">#{name}</a>}
+ %(<a href="#{url}" rel="nofollow noreferrer noopener" target="_blank">#{name}</a>)
end
it 'does not link the module name' do
diff --git a/spec/lib/gitlab/dependency_linker/gemfile_linker_spec.rb b/spec/lib/gitlab/dependency_linker/gemfile_linker_spec.rb
index 00e95dea224..a5507fab3fe 100644
--- a/spec/lib/gitlab/dependency_linker/gemfile_linker_spec.rb
+++ b/spec/lib/gitlab/dependency_linker/gemfile_linker_spec.rb
@@ -35,7 +35,7 @@ RSpec.describe Gitlab::DependencyLinker::GemfileLinker do
subject { Gitlab::Highlight.highlight(file_name, file_content) }
def link(name, url)
- %{<a href="#{url}" rel="nofollow noreferrer noopener" target="_blank">#{name}</a>}
+ %(<a href="#{url}" rel="nofollow noreferrer noopener" target="_blank">#{name}</a>)
end
it 'links sources' do
diff --git a/spec/lib/gitlab/dependency_linker/gemspec_linker_spec.rb b/spec/lib/gitlab/dependency_linker/gemspec_linker_spec.rb
index ae82dd51c95..9f207459113 100644
--- a/spec/lib/gitlab/dependency_linker/gemspec_linker_spec.rb
+++ b/spec/lib/gitlab/dependency_linker/gemspec_linker_spec.rb
@@ -42,7 +42,7 @@ RSpec.describe Gitlab::DependencyLinker::GemspecLinker do
subject { Gitlab::Highlight.highlight(file_name, file_content) }
def link(name, url)
- %{<a href="#{url}" rel="nofollow noreferrer noopener" target="_blank">#{name}</a>}
+ %(<a href="#{url}" rel="nofollow noreferrer noopener" target="_blank">#{name}</a>)
end
it 'does not link the gem name' do
diff --git a/spec/lib/gitlab/dependency_linker/go_mod_linker_spec.rb b/spec/lib/gitlab/dependency_linker/go_mod_linker_spec.rb
index 605b14bc923..fbd8b6477a2 100644
--- a/spec/lib/gitlab/dependency_linker/go_mod_linker_spec.rb
+++ b/spec/lib/gitlab/dependency_linker/go_mod_linker_spec.rb
@@ -55,7 +55,7 @@ RSpec.describe Gitlab::DependencyLinker::GoModLinker do
subject { Gitlab::Highlight.highlight(file_name, file_content) }
def link(name, url)
- %{<a href="#{url}" rel="nofollow noreferrer noopener" target="_blank">#{name}</a>}
+ %(<a href="#{url}" rel="nofollow noreferrer noopener" target="_blank">#{name}</a>)
end
it 'links the module name' do
diff --git a/spec/lib/gitlab/dependency_linker/go_sum_linker_spec.rb b/spec/lib/gitlab/dependency_linker/go_sum_linker_spec.rb
index 2836c0e9f29..559c27e91ba 100644
--- a/spec/lib/gitlab/dependency_linker/go_sum_linker_spec.rb
+++ b/spec/lib/gitlab/dependency_linker/go_sum_linker_spec.rb
@@ -35,7 +35,7 @@ RSpec.describe Gitlab::DependencyLinker::GoSumLinker do
subject { Gitlab::Highlight.highlight(file_name, file_content) }
def link(name, url)
- %{<a href="#{url}" rel="nofollow noreferrer noopener" target="_blank">#{name}</a>}
+ %(<a href="#{url}" rel="nofollow noreferrer noopener" target="_blank">#{name}</a>)
end
it 'links modules' do
diff --git a/spec/lib/gitlab/dependency_linker/godeps_json_linker_spec.rb b/spec/lib/gitlab/dependency_linker/godeps_json_linker_spec.rb
index c1ed030c548..3ac234f47d9 100644
--- a/spec/lib/gitlab/dependency_linker/godeps_json_linker_spec.rb
+++ b/spec/lib/gitlab/dependency_linker/godeps_json_linker_spec.rb
@@ -61,7 +61,7 @@ RSpec.describe Gitlab::DependencyLinker::GodepsJsonLinker do
subject { Gitlab::Highlight.highlight(file_name, file_content) }
def link(name, url)
- %{<a href="#{url}" rel="nofollow noreferrer noopener" target="_blank">#{name}</a>}
+ %(<a href="#{url}" rel="nofollow noreferrer noopener" target="_blank">#{name}</a>)
end
it 'links the package name' do
diff --git a/spec/lib/gitlab/dependency_linker/package_json_linker_spec.rb b/spec/lib/gitlab/dependency_linker/package_json_linker_spec.rb
index cdfc0e89bc7..127f437dd54 100644
--- a/spec/lib/gitlab/dependency_linker/package_json_linker_spec.rb
+++ b/spec/lib/gitlab/dependency_linker/package_json_linker_spec.rb
@@ -51,7 +51,7 @@ RSpec.describe Gitlab::DependencyLinker::PackageJsonLinker do
subject { Gitlab::Highlight.highlight(file_name, file_content) }
def link(name, url)
- %{<a href="#{url}" rel="nofollow noreferrer noopener" target="_blank">#{name}</a>}
+ %(<a href="#{url}" rel="nofollow noreferrer noopener" target="_blank">#{name}</a>)
end
it 'does not link the module name' do
diff --git a/spec/lib/gitlab/dependency_linker/podfile_linker_spec.rb b/spec/lib/gitlab/dependency_linker/podfile_linker_spec.rb
index 8e536c00ea6..41c29278bda 100644
--- a/spec/lib/gitlab/dependency_linker/podfile_linker_spec.rb
+++ b/spec/lib/gitlab/dependency_linker/podfile_linker_spec.rb
@@ -35,7 +35,7 @@ RSpec.describe Gitlab::DependencyLinker::PodfileLinker do
subject { Gitlab::Highlight.highlight(file_name, file_content) }
def link(name, url)
- %{<a href="#{url}" rel="nofollow noreferrer noopener" target="_blank">#{name}</a>}
+ %(<a href="#{url}" rel="nofollow noreferrer noopener" target="_blank">#{name}</a>)
end
it 'links sources' do
diff --git a/spec/lib/gitlab/dependency_linker/podspec_json_linker_spec.rb b/spec/lib/gitlab/dependency_linker/podspec_json_linker_spec.rb
index 1f81049a41e..f8b782a7cda 100644
--- a/spec/lib/gitlab/dependency_linker/podspec_json_linker_spec.rb
+++ b/spec/lib/gitlab/dependency_linker/podspec_json_linker_spec.rb
@@ -66,7 +66,7 @@ RSpec.describe Gitlab::DependencyLinker::PodspecJsonLinker do
subject { Gitlab::Highlight.highlight(file_name, file_content) }
def link(name, url)
- %{<a href="#{url}" rel="nofollow noreferrer noopener" target="_blank">#{name}</a>}
+ %(<a href="#{url}" rel="nofollow noreferrer noopener" target="_blank">#{name}</a>)
end
it 'links the gem name' do
diff --git a/spec/lib/gitlab/dependency_linker/podspec_linker_spec.rb b/spec/lib/gitlab/dependency_linker/podspec_linker_spec.rb
index 132b5b21d85..6f2653829e2 100644
--- a/spec/lib/gitlab/dependency_linker/podspec_linker_spec.rb
+++ b/spec/lib/gitlab/dependency_linker/podspec_linker_spec.rb
@@ -41,7 +41,7 @@ RSpec.describe Gitlab::DependencyLinker::PodspecLinker do
subject { Gitlab::Highlight.highlight(file_name, file_content) }
def link(name, url)
- %{<a href="#{url}" rel="nofollow noreferrer noopener" target="_blank">#{name}</a>}
+ %(<a href="#{url}" rel="nofollow noreferrer noopener" target="_blank">#{name}</a>)
end
it 'does not link the pod name' do
diff --git a/spec/lib/gitlab/dependency_linker/requirements_txt_linker_spec.rb b/spec/lib/gitlab/dependency_linker/requirements_txt_linker_spec.rb
index 86ebddc9681..fc3c57b7cff 100644
--- a/spec/lib/gitlab/dependency_linker/requirements_txt_linker_spec.rb
+++ b/spec/lib/gitlab/dependency_linker/requirements_txt_linker_spec.rb
@@ -64,7 +64,7 @@ RSpec.describe Gitlab::DependencyLinker::RequirementsTxtLinker do
subject { Gitlab::Highlight.highlight(file_name, file_content) }
def link(name, url)
- %{<a href="#{url}" rel="nofollow noreferrer noopener" target="_blank">#{name}</a>}
+ %(<a href="#{url}" rel="nofollow noreferrer noopener" target="_blank">#{name}</a>)
end
it 'links dependencies' do
diff --git a/spec/lib/gitlab/diff/file_collection/compare_spec.rb b/spec/lib/gitlab/diff/file_collection/compare_spec.rb
index c3f768db7f0..5469a43e46e 100644
--- a/spec/lib/gitlab/diff/file_collection/compare_spec.rb
+++ b/spec/lib/gitlab/diff/file_collection/compare_spec.rb
@@ -10,9 +10,11 @@ RSpec.describe Gitlab::Diff::FileCollection::Compare do
let(:start_commit) { sample_image_commit }
let(:head_commit) { sample_commit }
let(:raw_compare) do
- Gitlab::Git::Compare.new(project.repository.raw_repository,
- start_commit.id,
- head_commit.id)
+ Gitlab::Git::Compare.new(
+ project.repository.raw_repository,
+ start_commit.id,
+ head_commit.id
+ )
end
let(:diffable) { Compare.new(raw_compare, project) }
diff --git a/spec/lib/gitlab/diff/file_collection/merge_request_diff_batch_spec.rb b/spec/lib/gitlab/diff/file_collection/merge_request_diff_batch_spec.rb
index 8e14f48ae29..65e96f8e936 100644
--- a/spec/lib/gitlab/diff/file_collection/merge_request_diff_batch_spec.rb
+++ b/spec/lib/gitlab/diff/file_collection/merge_request_diff_batch_spec.rb
@@ -10,10 +10,7 @@ RSpec.describe Gitlab::Diff::FileCollection::MergeRequestDiffBatch, feature_cate
let(:diff_files_relation) { diffable.merge_request_diff_files }
subject do
- described_class.new(diffable,
- batch_page,
- batch_size,
- diff_options: nil)
+ described_class.new(diffable, batch_page, batch_size, diff_options: nil)
end
let(:diff_files) { subject.diff_files }
@@ -87,10 +84,7 @@ RSpec.describe Gitlab::Diff::FileCollection::MergeRequestDiffBatch, feature_cate
context 'last page' do
it 'returns correct diff files' do
last_page = diff_files_relation.count - batch_size
- collection = described_class.new(diffable,
- last_page,
- batch_size,
- diff_options: nil)
+ collection = described_class.new(diffable, last_page, batch_size, diff_options: nil)
expected_batch_files = diff_files_relation.offset(last_page).limit(batch_size).map(&:new_path)
@@ -101,10 +95,7 @@ RSpec.describe Gitlab::Diff::FileCollection::MergeRequestDiffBatch, feature_cate
it_behaves_like 'unfoldable diff' do
subject do
- described_class.new(merge_request.merge_request_diff,
- batch_page,
- batch_size,
- diff_options: nil)
+ described_class.new(merge_request.merge_request_diff, batch_page, batch_size, diff_options: nil)
end
end
@@ -118,10 +109,7 @@ RSpec.describe Gitlab::Diff::FileCollection::MergeRequestDiffBatch, feature_cate
let(:stub_path) { '.gitignore' }
subject do
- described_class.new(merge_request.merge_request_diff,
- batch_page,
- batch_size,
- **collection_default_args)
+ described_class.new(merge_request.merge_request_diff, batch_page, batch_size, **collection_default_args)
end
end
@@ -136,10 +124,7 @@ RSpec.describe Gitlab::Diff::FileCollection::MergeRequestDiffBatch, feature_cate
end
subject do
- described_class.new(merge_request.merge_request_diff,
- batch_page,
- batch_size,
- **collection_default_args)
+ described_class.new(merge_request.merge_request_diff, batch_page, batch_size, **collection_default_args)
end
end
end
diff --git a/spec/lib/gitlab/diff/file_collection/paginated_merge_request_diff_spec.rb b/spec/lib/gitlab/diff/file_collection/paginated_merge_request_diff_spec.rb
index ee956d04325..891336658ce 100644
--- a/spec/lib/gitlab/diff/file_collection/paginated_merge_request_diff_spec.rb
+++ b/spec/lib/gitlab/diff/file_collection/paginated_merge_request_diff_spec.rb
@@ -11,9 +11,7 @@ RSpec.describe Gitlab::Diff::FileCollection::PaginatedMergeRequestDiff, feature_
let(:diff_files) { subject.diff_files }
subject do
- described_class.new(diffable,
- page,
- per_page)
+ described_class.new(diffable, page, per_page)
end
describe '#diff_files' do
@@ -79,9 +77,7 @@ RSpec.describe Gitlab::Diff::FileCollection::PaginatedMergeRequestDiff, feature_
context 'when last page' do
it 'returns correct diff files' do
last_page = diff_files_relation.count - per_page
- collection = described_class.new(diffable,
- last_page,
- per_page)
+ collection = described_class.new(diffable, last_page, per_page)
expected_batch_files = diff_files_relation.page(last_page).per(per_page).map(&:new_path)
@@ -92,9 +88,7 @@ RSpec.describe Gitlab::Diff::FileCollection::PaginatedMergeRequestDiff, feature_
it_behaves_like 'unfoldable diff' do
subject do
- described_class.new(merge_request.merge_request_diff,
- page,
- per_page)
+ described_class.new(merge_request.merge_request_diff, page, per_page)
end
end
@@ -106,9 +100,7 @@ RSpec.describe Gitlab::Diff::FileCollection::PaginatedMergeRequestDiff, feature_
let(:diffable) { merge_request.merge_request_diff }
subject do
- described_class.new(merge_request.merge_request_diff,
- page,
- per_page)
+ described_class.new(merge_request.merge_request_diff, page, per_page)
end
end
end
diff --git a/spec/lib/gitlab/diff/file_spec.rb b/spec/lib/gitlab/diff/file_spec.rb
index ad2524e40c5..bc4fc49b1b7 100644
--- a/spec/lib/gitlab/diff/file_spec.rb
+++ b/spec/lib/gitlab/diff/file_spec.rb
@@ -407,10 +407,7 @@ RSpec.describe Gitlab::Diff::File do
context 'diff file stats' do
let(:diff_file) do
- described_class.new(diff,
- diff_refs: commit.diff_refs,
- repository: project.repository,
- stats: stats)
+ described_class.new(diff, diff_refs: commit.diff_refs, repository: project.repository, stats: stats)
end
let(:raw_diff) do
diff --git a/spec/lib/gitlab/diff/formatters/file_formatter_spec.rb b/spec/lib/gitlab/diff/formatters/file_formatter_spec.rb
index 32e5f17f7eb..fc77ed5d763 100644
--- a/spec/lib/gitlab/diff/formatters/file_formatter_spec.rb
+++ b/spec/lib/gitlab/diff/formatters/file_formatter_spec.rb
@@ -17,7 +17,7 @@ RSpec.describe Gitlab::Diff::Formatters::FileFormatter, feature_category: :code_
let(:attrs) { base_attrs.merge(old_path: 'path.rb', new_path: 'path.rb') }
it_behaves_like 'position formatter' do
- # rubocop:disable Fips/SHA1 (This is used to match the existing class method)
+ # rubocop:disable Fips/SHA1 -- This is used to match the existing class method
let(:key) do
[123, 456, 789,
Digest::SHA1.hexdigest(formatter.old_path), Digest::SHA1.hexdigest(formatter.new_path),
diff --git a/spec/lib/gitlab/diff/highlight_cache_spec.rb b/spec/lib/gitlab/diff/highlight_cache_spec.rb
index c51eaa4fa18..94a5d30283c 100644
--- a/spec/lib/gitlab/diff/highlight_cache_spec.rb
+++ b/spec/lib/gitlab/diff/highlight_cache_spec.rb
@@ -49,10 +49,12 @@ RSpec.describe Gitlab::Diff::HighlightCache, :clean_gitlab_redis_cache, feature_
let(:diff_file) do
diffs = merge_request.diffs
raw_diff = diffs.diffable.raw_diffs(diffs.diff_options.merge(paths: ['CHANGELOG'])).first
- Gitlab::Diff::File.new(raw_diff,
- repository: diffs.project.repository,
- diff_refs: diffs.diff_refs,
- fallback_diff_refs: diffs.fallback_diff_refs)
+ Gitlab::Diff::File.new(
+ raw_diff,
+ repository: diffs.project.repository,
+ diff_refs: diffs.diff_refs,
+ fallback_diff_refs: diffs.fallback_diff_refs
+ )
end
before do
@@ -227,10 +229,12 @@ RSpec.describe Gitlab::Diff::HighlightCache, :clean_gitlab_redis_cache, feature_
let(:diff_file) do
diffs = merge_request.diffs
raw_diff = diffs.diffable.raw_diffs(diffs.diff_options.merge(paths: ['CHANGELOG'])).first
- Gitlab::Diff::File.new(raw_diff,
- repository: diffs.project.repository,
- diff_refs: diffs.diff_refs,
- fallback_diff_refs: diffs.fallback_diff_refs)
+ Gitlab::Diff::File.new(
+ raw_diff,
+ repository: diffs.project.repository,
+ diff_refs: diffs.diff_refs,
+ fallback_diff_refs: diffs.fallback_diff_refs
+ )
end
it "uses ActiveSupport::Gzip when reading from the cache" do
diff --git a/spec/lib/gitlab/diff/highlight_spec.rb b/spec/lib/gitlab/diff/highlight_spec.rb
index e39c15c8fd7..e65f5a618a5 100644
--- a/spec/lib/gitlab/diff/highlight_spec.rb
+++ b/spec/lib/gitlab/diff/highlight_spec.rb
@@ -44,13 +44,13 @@ RSpec.describe Gitlab::Diff::Highlight, feature_category: :source_code_managemen
end
it 'highlights and marks removed lines' do
- code = %{-<span id="LC9" class="line" lang="ruby"> <span class="k">raise</span> <span class="s2">"System commands must be given as an array of strings"</span></span>\n}
+ code = %(-<span id="LC9" class="line" lang="ruby"> <span class="k">raise</span> <span class="s2">"System commands must be given as an array of strings"</span></span>\n)
expect(subject[4].rich_text).to eq(code)
end
it 'highlights and marks added lines' do
- code = %{+<span id="LC9" class="line" lang="ruby"> <span class="k">raise</span> <span class="no"><span class="idiff left addition">RuntimeError</span></span><span class="p"><span class="idiff addition">,</span></span><span class="idiff right addition"> </span><span class="s2">"System commands must be given as an array of strings"</span></span>\n}
+ code = %(+<span id="LC9" class="line" lang="ruby"> <span class="k">raise</span> <span class="no"><span class="idiff left addition">RuntimeError</span></span><span class="p"><span class="idiff addition">,</span></span><span class="idiff right addition"> </span><span class="s2">"System commands must be given as an array of strings"</span></span>\n)
expect(subject[5].rich_text).to eq(code)
end
@@ -86,14 +86,14 @@ RSpec.describe Gitlab::Diff::Highlight, feature_category: :source_code_managemen
end
it 'marks removed lines' do
- code = %q{- raise "System commands must be given as an array of strings"}
+ code = %q(- raise "System commands must be given as an array of strings")
expect(subject[4].text).to eq(code)
expect(subject[4].text).not_to be_html_safe
end
it 'marks added lines' do
- code = %q{+ raise <span class="idiff left right addition">RuntimeError, </span>&quot;System commands must be given as an array of strings&quot;}
+ code = %q(+ raise <span class="idiff left right addition">RuntimeError, </span>&quot;System commands must be given as an array of strings&quot;)
expect(subject[5].rich_text).to eq(code)
expect(subject[5].rich_text).to be_html_safe
@@ -107,7 +107,7 @@ RSpec.describe Gitlab::Diff::Highlight, feature_category: :source_code_managemen
it 'keeps the original rich line' do
allow(Gitlab::ErrorTracking).to receive(:track_and_raise_for_dev_exception)
- code = %q{+ raise RuntimeError, "System commands must be given as an array of strings"}
+ code = %q(+ raise RuntimeError, "System commands must be given as an array of strings")
expect(subject[5].text).to eq(code)
expect(subject[5].text).not_to be_html_safe
diff --git a/spec/lib/gitlab/diff/inline_diff_marker_spec.rb b/spec/lib/gitlab/diff/inline_diff_marker_spec.rb
index 8ab2a7b64dd..602f1b1c2b2 100644
--- a/spec/lib/gitlab/diff/inline_diff_marker_spec.rb
+++ b/spec/lib/gitlab/diff/inline_diff_marker_spec.rb
@@ -10,10 +10,10 @@ RSpec.describe Gitlab::Diff::InlineDiffMarker do
subject { described_class.new(raw, rich).mark(inline_diffs) }
context "when the rich text is html safe" do
- let(:rich) { %{<span class="abc">abc</span><span class="space"> </span><span class="def">&#39;def&#39;</span>}.html_safe }
+ let(:rich) { %(<span class="abc">abc</span><span class="space"> </span><span class="def">&#39;def&#39;</span>).html_safe }
it 'marks the range' do
- expect(subject).to eq(%{<span class="abc">ab<span class="idiff left">c</span></span><span class="space"><span class="idiff"> </span></span><span class="def"><span class="idiff right">&#39;d</span>ef&#39;</span>})
+ expect(subject).to eq(%(<span class="abc">ab<span class="idiff left">c</span></span><span class="space"><span class="idiff"> </span></span><span class="def"><span class="idiff right">&#39;d</span>ef&#39;</span>))
expect(subject).to be_html_safe
end
end
@@ -22,7 +22,7 @@ RSpec.describe Gitlab::Diff::InlineDiffMarker do
let(:rich) { "abc 'def' differs" }
it 'marks the range' do
- expect(subject).to eq(%{ab<span class="idiff left right">c &#39;d</span>ef&#39; differs})
+ expect(subject).to eq(%(ab<span class="idiff left right">c &#39;d</span>ef&#39; differs))
expect(subject).to be_html_safe
end
end
diff --git a/spec/lib/gitlab/diff/line_spec.rb b/spec/lib/gitlab/diff/line_spec.rb
index 949def599ae..b23ba3a0f00 100644
--- a/spec/lib/gitlab/diff/line_spec.rb
+++ b/spec/lib/gitlab/diff/line_spec.rb
@@ -11,10 +11,16 @@ RSpec.describe Gitlab::Diff::Line do
end
let(:line) do
- described_class.new('<input>', 'match', 0, 0, 1,
- parent_file: double(:file),
- line_code: double(:line_code),
- rich_text: rich_text)
+ described_class.new(
+ '<input>',
+ 'match',
+ 0,
+ 0,
+ 1,
+ parent_file: double(:file),
+ line_code: double(:line_code),
+ rich_text: rich_text
+ )
end
let(:rich_text) { nil }
diff --git a/spec/lib/gitlab/diff/suggestion_diff_spec.rb b/spec/lib/gitlab/diff/suggestion_diff_spec.rb
index 9546c581112..f9d56662753 100644
--- a/spec/lib/gitlab/diff/suggestion_diff_spec.rb
+++ b/spec/lib/gitlab/diff/suggestion_diff_spec.rb
@@ -22,9 +22,7 @@ RSpec.describe Gitlab::Diff::SuggestionDiff do
end
let(:suggestion) do
- instance_double(Suggestion, from_line: 12,
- from_content: from_content,
- to_content: to_content)
+ instance_double(Suggestion, from_line: 12, from_content: from_content, to_content: to_content)
end
subject { described_class.new(suggestion).diff_lines }
@@ -56,9 +54,12 @@ RSpec.describe Gitlab::Diff::SuggestionDiff do
it 'returns a correct value if there is no newline at the end of the file' do
from_content = "One line test"
to_content = "Successful test!"
- suggestion = instance_double(Suggestion, from_line: 1,
- from_content: from_content,
- to_content: to_content)
+ suggestion = instance_double(
+ Suggestion,
+ from_line: 1,
+ from_content: from_content,
+ to_content: to_content
+ )
diff_lines = described_class.new(suggestion).diff_lines
diff --git a/spec/lib/gitlab/diff/suggestion_spec.rb b/spec/lib/gitlab/diff/suggestion_spec.rb
index 40779faf917..9f654c44852 100644
--- a/spec/lib/gitlab/diff/suggestion_spec.rb
+++ b/spec/lib/gitlab/diff/suggestion_spec.rb
@@ -5,10 +5,12 @@ require 'spec_helper'
RSpec.describe Gitlab::Diff::Suggestion do
shared_examples 'correct suggestion raw content' do
it 'returns correct raw data' do
- expect(suggestion.to_hash).to include(from_content: expected_lines.join,
- to_content: "#{text}\n",
- lines_above: above,
- lines_below: below)
+ expect(suggestion.to_hash).to include(
+ from_content: expected_lines.join,
+ to_content: "#{text}\n",
+ lines_above: above,
+ lines_below: below
+ )
end
it 'returns diff lines with correct line numbers' do
@@ -25,11 +27,13 @@ RSpec.describe Gitlab::Diff::Suggestion do
let(:merge_request) { create(:merge_request) }
let(:project) { merge_request.project }
let(:position) do
- Gitlab::Diff::Position.new(old_path: "files/ruby/popen.rb",
- new_path: "files/ruby/popen.rb",
- old_line: nil,
- new_line: 9,
- diff_refs: merge_request.diff_refs)
+ Gitlab::Diff::Position.new(
+ old_path: "files/ruby/popen.rb",
+ new_path: "files/ruby/popen.rb",
+ old_line: nil,
+ new_line: 9,
+ diff_refs: merge_request.diff_refs
+ )
end
let(:diff_file) do
diff --git a/spec/lib/gitlab/diff/suggestions_parser_spec.rb b/spec/lib/gitlab/diff/suggestions_parser_spec.rb
index a00c55d4fb2..ef845dbdc4c 100644
--- a/spec/lib/gitlab/diff/suggestions_parser_spec.rb
+++ b/spec/lib/gitlab/diff/suggestions_parser_spec.rb
@@ -7,11 +7,13 @@ RSpec.describe Gitlab::Diff::SuggestionsParser do
let(:merge_request) { create(:merge_request) }
let(:project) { merge_request.project }
let(:position) do
- Gitlab::Diff::Position.new(old_path: "files/ruby/popen.rb",
- new_path: "files/ruby/popen.rb",
- old_line: nil,
- new_line: 9,
- diff_refs: merge_request.diff_refs)
+ Gitlab::Diff::Position.new(
+ old_path: "files/ruby/popen.rb",
+ new_path: "files/ruby/popen.rb",
+ old_line: nil,
+ new_line: 9,
+ diff_refs: merge_request.diff_refs
+ )
end
let(:diff_file) do
@@ -19,8 +21,7 @@ RSpec.describe Gitlab::Diff::SuggestionsParser do
end
subject do
- described_class.parse(markdown, project: merge_request.project,
- position: position)
+ described_class.parse(markdown, project: merge_request.project, position: position)
end
def blob_lines_data(from_line, to_line)
@@ -59,15 +60,19 @@ RSpec.describe Gitlab::Diff::SuggestionsParser do
from_line = position.new_line
to_line = position.new_line
- expect(subject.first.to_hash).to include(from_content: blob_lines_data(from_line, to_line),
- to_content: " foo\n bar\n",
- lines_above: 0,
- lines_below: 0)
-
- expect(subject.second.to_hash).to include(from_content: blob_lines_data(from_line, to_line),
- to_content: " xpto\n baz\n",
- lines_above: 0,
- lines_below: 0)
+ expect(subject.first.to_hash).to include(
+ from_content: blob_lines_data(from_line, to_line),
+ to_content: " foo\n bar\n",
+ lines_above: 0,
+ lines_below: 0
+ )
+
+ expect(subject.second.to_hash).to include(
+ from_content: blob_lines_data(from_line, to_line),
+ to_content: " xpto\n baz\n",
+ lines_above: 0,
+ lines_below: 0
+ )
end
end
@@ -105,30 +110,36 @@ RSpec.describe Gitlab::Diff::SuggestionsParser do
from_line = position.new_line - 2
to_line = position.new_line + 1
- expect(subject.first.to_hash).to include(from_content: blob_lines_data(from_line, to_line),
- to_content: " # above and below\n",
- lines_above: 2,
- lines_below: 1)
+ expect(subject.first.to_hash).to include(
+ from_content: blob_lines_data(from_line, to_line),
+ to_content: " # above and below\n",
+ lines_above: 2,
+ lines_below: 1
+ )
end
it 'suggestion with above param has correct data' do
from_line = position.new_line - 3
to_line = position.new_line
- expect(subject.second.to_hash).to eq(from_content: blob_lines_data(from_line, to_line),
- to_content: " # only above\n",
- lines_above: 3,
- lines_below: 0)
+ expect(subject.second.to_hash).to eq(
+ from_content: blob_lines_data(from_line, to_line),
+ to_content: " # only above\n",
+ lines_above: 3,
+ lines_below: 0
+ )
end
it 'suggestion with below param has correct data' do
from_line = position.new_line
to_line = position.new_line + 3
- expect(subject.third.to_hash).to eq(from_content: blob_lines_data(from_line, to_line),
- to_content: " # only below\n",
- lines_above: 0,
- lines_below: 3)
+ expect(subject.third.to_hash).to eq(
+ from_content: blob_lines_data(from_line, to_line),
+ to_content: " # only below\n",
+ lines_above: 0,
+ lines_below: 3
+ )
end
end
end
diff --git a/spec/lib/gitlab/discussions_diff/file_collection_spec.rb b/spec/lib/gitlab/discussions_diff/file_collection_spec.rb
index f85a68ada15..a1d9a861355 100644
--- a/spec/lib/gitlab/discussions_diff/file_collection_spec.rb
+++ b/spec/lib/gitlab/discussions_diff/file_collection_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::DiscussionsDiff::FileCollection do
+RSpec.describe Gitlab::DiscussionsDiff::FileCollection, :clean_gitlab_redis_shared_state do
let(:merge_request) { create(:merge_request) }
let!(:diff_note_a) { create(:diff_note_on_merge_request, project: merge_request.project, noteable: merge_request) }
let!(:diff_note_b) { create(:diff_note_on_merge_request, project: merge_request.project, noteable: merge_request) }
@@ -11,7 +11,18 @@ RSpec.describe Gitlab::DiscussionsDiff::FileCollection do
subject { described_class.new([note_diff_file_a, note_diff_file_b]) }
- describe '#load_highlight', :clean_gitlab_redis_shared_state do
+ describe '#load_highlight' do
+ it 'only takes into account for the specific diff note ids' do
+ file_a_caching_content = diff_note_a.diff_file.highlighted_diff_lines.map(&:to_hash)
+
+ expect(Gitlab::DiscussionsDiff::HighlightCache)
+ .to receive(:write_multiple)
+ .with({ note_diff_file_a.id => file_a_caching_content })
+ .and_call_original
+
+ subject.load_highlight(diff_note_ids: [note_diff_file_a.diff_note_id])
+ end
+
it 'writes uncached diffs highlight' do
file_a_caching_content = diff_note_a.diff_file.highlighted_diff_lines.map(&:to_hash)
file_b_caching_content = diff_note_b.diff_file.highlighted_diff_lines.map(&:to_hash)
diff --git a/spec/lib/gitlab/email/handler/service_desk_handler_spec.rb b/spec/lib/gitlab/email/handler/service_desk_handler_spec.rb
index e6fff939632..f13fd0be4cd 100644
--- a/spec/lib/gitlab/email/handler/service_desk_handler_spec.rb
+++ b/spec/lib/gitlab/email/handler/service_desk_handler_spec.rb
@@ -8,12 +8,14 @@ RSpec.describe Gitlab::Email::Handler::ServiceDeskHandler, feature_category: :se
before do
stub_incoming_email_setting(enabled: true, address: "incoming+%{key}@appmail.adventuretime.ooo")
+ stub_service_desk_email_setting(enabled: true, address: "contact+%{key}@example.com")
stub_config_setting(host: 'localhost')
end
let(:email_raw) { email_fixture('emails/service_desk.eml') }
let(:author_email) { 'jake@adventuretime.ooo' }
let(:message_id) { 'CADkmRc+rNGAGGbV2iE5p918UVy4UyJqVcXRO2=otppgzduJSg@mail.gmail.com' }
+ let(:issue_email_participants_count) { 1 }
let_it_be(:group) { create(:group, :private, :crm_enabled, name: "email") }
@@ -22,7 +24,7 @@ RSpec.describe Gitlab::Email::Handler::ServiceDeskHandler, feature_category: :se
end
context 'service desk is enabled for the project' do
- let_it_be(:project) { create(:project, :repository, :private, group: group, path: 'test', service_desk_enabled: true) }
+ let_it_be_with_reload(:project) { create(:project, :repository, :private, group: group, path: 'test', service_desk_enabled: true) }
before do
allow(Gitlab::ServiceDesk).to receive(:supported?).and_return(true)
@@ -50,6 +52,7 @@ RSpec.describe Gitlab::Email::Handler::ServiceDeskHandler, feature_category: :se
receiver.execute
new_issue = Issue.last
+ expect(new_issue.issue_email_participants.count).to eq(issue_email_participants_count)
expect(new_issue.issue_email_participants.first.email).to eq(author_email)
end
@@ -96,6 +99,72 @@ RSpec.describe Gitlab::Email::Handler::ServiceDeskHandler, feature_category: :se
expect(new_issue.issue_customer_relations_contacts.map(&:contact)).to contain_exactly(contact, contact2, contact3)
end
+ context 'when add_external_participants_from_cc is true' do
+ shared_examples 'does not add CC address' do
+ it 'creates a new issue and adds issue_email_participant from From header' do
+ expect { receiver.execute }.to change { Issue.count }.by(1)
+ expect(Issue.last.issue_email_participants.map(&:email)).to match_array(%w[from@example.com])
+ end
+ end
+
+ let_it_be(:setting) { create(:service_desk_setting, project: project, add_external_participants_from_cc: true) }
+
+ # Original email contains two CC email addresses
+ let(:issue_email_participants_count) { 3 }
+ let(:to_address) { project.service_desk_incoming_address }
+
+ it_behaves_like 'a new issue request'
+
+ context 'when no CC header is present' do
+ let(:email_raw) do
+ <<~EMAIL
+ From: from@example.com
+ To: #{to_address}
+ Subject: Issue title
+
+ Issue description
+ EMAIL
+ end
+
+ it_behaves_like 'does not add CC address'
+ end
+
+ context 'when service desk system address is in CC' do
+ let(:cc_address) { project.service_desk_incoming_address }
+ let(:email_raw) do
+ <<~EMAIL
+ From: from@example.com
+ To: #{to_address}
+ Cc: #{cc_address}
+ Subject: Issue title
+
+ Issue description
+ EMAIL
+ end
+
+ it_behaves_like 'does not add CC address'
+
+ context 'when service_desk_email is part of CC' do
+ let(:cc_address) { project.service_desk_alias_address }
+
+ it_behaves_like 'does not add CC address'
+ end
+
+ context 'when custom email is part of CC' do
+ let!(:credential) { create(:service_desk_custom_email_credential, project: project) }
+ let!(:verification) { create(:service_desk_custom_email_verification, :finished, project: project) }
+ let(:cc_address) { project.service_desk_custom_address }
+
+ before do
+ project.reset
+ setting.update!(custom_email: 'support@example.com', custom_email_enabled: true)
+ end
+
+ it_behaves_like 'does not add CC address'
+ end
+ end
+ end
+
context 'with legacy incoming email address' do
let(:email_raw) { fixture_file('emails/service_desk_legacy.eml') }
@@ -140,25 +209,26 @@ RSpec.describe Gitlab::Email::Handler::ServiceDeskHandler, feature_category: :se
subject
end
- context 'when issue_email_participants FF is enabled' do
- it 'creates 2 issue_email_participants' do
- subject
+ it 'creates issue_email_participants for author and reply author' do
+ subject
- expect(Issue.last.issue_email_participants.map(&:email))
- .to match_array(%w(alan@adventuretime.ooo jake@adventuretime.ooo))
- end
+ # 1 from issue creation
+ # 1 from new note reply
+ expect(Issue.last.issue_email_participants.map(&:email))
+ .to match_array(%w[alan@adventuretime.ooo jake@adventuretime.ooo])
end
context 'when issue_email_participants FF is disabled' do
before do
+ # Was turned off after issue creation
stub_feature_flags(issue_email_participants: false)
end
- it 'creates only 1 issue_email_participant' do
+ it 'creates issue_email_participant for the author' do
subject
expect(Issue.last.issue_email_participants.map(&:email))
- .to match_array(%w(jake@adventuretime.ooo))
+ .to match_array(%w[jake@adventuretime.ooo])
end
end
end
@@ -182,11 +252,11 @@ RSpec.describe Gitlab::Email::Handler::ServiceDeskHandler, feature_category: :se
subject
end
- it 'creates 1 issue_email_participant' do
+ it 'creates issue_email_participant for the author' do
subject
expect(Issue.last.issue_email_participants.map(&:email))
- .to match_array(%w(alan@adventuretime.ooo))
+ .to match_array(%w[alan@adventuretime.ooo])
end
end
end
diff --git a/spec/lib/gitlab/email/handler_spec.rb b/spec/lib/gitlab/email/handler_spec.rb
index d3a4d77c58e..47907401a63 100644
--- a/spec/lib/gitlab/email/handler_spec.rb
+++ b/spec/lib/gitlab/email/handler_spec.rb
@@ -60,10 +60,10 @@ RSpec.describe Gitlab::Email::Handler do
describe 'regexps are set properly' do
let(:addresses) do
- %W(sent_notification_key#{Gitlab::Email::Common::UNSUBSCRIBE_SUFFIX} sent_notification_key#{Gitlab::Email::Common::UNSUBSCRIBE_SUFFIX_LEGACY}) +
- %w(sent_notification_key path-to-project-123-user_email_token-merge-request) +
- %w(path-to-project-123-user_email_token-issue path-to-project-123-user_email_token-issue-123) +
- %w(path/to/project+user_email_token path/to/project+merge-request+user_email_token some/project)
+ %W[sent_notification_key#{Gitlab::Email::Common::UNSUBSCRIBE_SUFFIX} sent_notification_key#{Gitlab::Email::Common::UNSUBSCRIBE_SUFFIX_LEGACY}] +
+ %w[sent_notification_key path-to-project-123-user_email_token-merge-request] +
+ %w[path-to-project-123-user_email_token-issue path-to-project-123-user_email_token-issue-123] +
+ %w[path/to/project+user_email_token path/to/project+merge-request+user_email_token some/project]
end
before do
diff --git a/spec/lib/gitlab/email/receiver_spec.rb b/spec/lib/gitlab/email/receiver_spec.rb
index f8084d24850..c86a83092a4 100644
--- a/spec/lib/gitlab/email/receiver_spec.rb
+++ b/spec/lib/gitlab/email/receiver_spec.rb
@@ -33,7 +33,7 @@ RSpec.describe Gitlab::Email::Receiver do
metadata = receiver.mail_metadata
- expect(metadata.keys).to match_array(%i(mail_uid from_address to_address mail_key references delivered_to envelope_to x_envelope_to meta received_recipients cc_address))
+ expect(metadata.keys).to match_array(%i[mail_uid from_address to_address mail_key references delivered_to envelope_to x_envelope_to meta received_recipients cc_address])
expect(metadata[:meta]).to include(client_id: client_id, project: project.full_path)
expect(metadata[meta_key]).to eq(meta_value)
end
diff --git a/spec/lib/gitlab/encoding_helper_spec.rb b/spec/lib/gitlab/encoding_helper_spec.rb
index 1b7c11dfef6..db7961fc0c9 100644
--- a/spec/lib/gitlab/encoding_helper_spec.rb
+++ b/spec/lib/gitlab/encoding_helper_spec.rb
@@ -214,7 +214,7 @@ RSpec.describe Gitlab::EncodingHelper, feature_category: :shared do
[nil, ""],
["", ""],
[" ", " "],
- %w(a1 a1),
+ %w[a1 a1],
["编码", "\xE7\xBC\x96\xE7\xA0\x81".b]
].each do |input, result|
it "encodes #{input.inspect} to #{result.inspect}" do
diff --git a/spec/lib/gitlab/endpoint_attributes_spec.rb b/spec/lib/gitlab/endpoint_attributes_spec.rb
index a623070c3eb..34f4221b86a 100644
--- a/spec/lib/gitlab/endpoint_attributes_spec.rb
+++ b/spec/lib/gitlab/endpoint_attributes_spec.rb
@@ -11,19 +11,19 @@ RSpec.describe Gitlab::EndpointAttributes, feature_category: :api do
let(:controller) do
Class.new(base_controller) do
- feature_category :foo, %w(update edit)
- feature_category :bar, %w(index show)
- feature_category :quux, %w(destroy)
+ feature_category :foo, %w[update edit]
+ feature_category :bar, %w[index show]
+ feature_category :quux, %w[destroy]
- urgency :high, %w(do_a)
- urgency :low, %w(do_b do_c)
+ urgency :high, %w[do_a]
+ urgency :low, %w[do_b do_c]
end
end
let(:subclass) do
Class.new(controller) do
- feature_category :baz, %w(subclass_index)
- urgency :high, %w(superclass_do_something)
+ feature_category :baz, %w[subclass_index]
+ urgency :high, %w[superclass_do_something]
end
end
diff --git a/spec/lib/gitlab/error_tracking/stack_trace_highlight_decorator_spec.rb b/spec/lib/gitlab/error_tracking/stack_trace_highlight_decorator_spec.rb
index a854adca32b..eae6186e789 100644
--- a/spec/lib/gitlab/error_tracking/stack_trace_highlight_decorator_spec.rb
+++ b/spec/lib/gitlab/error_tracking/stack_trace_highlight_decorator_spec.rb
@@ -28,7 +28,7 @@ RSpec.describe Gitlab::ErrorTracking::StackTraceHighlightDecorator do
[11, '<span id="LC1" class="line" lang="ruby"><span class="k">class</span> <span class="nc">HelloWorld</span></span>'],
[12, '<span id="LC1" class="line" lang="ruby"> <span class="k">def</span> <span class="nc">self</span><span class="o">.</span><span class="nf">message</span></span>'],
[13, '<span id="LC1" class="line" lang="ruby"> <span class="vi">@name</span> <span class="o">=</span> <span class="s1">\'World\'</span></span>'],
- [14, %[<span id="LC1" class="line" lang="ruby"> <span class="nb">puts</span> <span class="s2">"Hello </span><span class="si">\#{</span><span class="vi">@name</span><span class="si">}</span><span class="s2">"</span></span>]],
+ [14, %(<span id="LC1" class="line" lang="ruby"> <span class="nb">puts</span> <span class="s2">"Hello </span><span class="si">\#{</span><span class="vi">@name</span><span class="si">}</span><span class="s2">"</span></span>)],
[15, '<span id="LC1" class="line" lang="ruby"> <span class="k">end</span></span>'],
[16, '<span id="LC1" class="line" lang="ruby"><span class="k">end</span></span>']
]
diff --git a/spec/lib/gitlab/external_authorization/client_spec.rb b/spec/lib/gitlab/external_authorization/client_spec.rb
index b907b0bb262..b507fe7bde8 100644
--- a/spec/lib/gitlab/external_authorization/client_spec.rb
+++ b/spec/lib/gitlab/external_authorization/client_spec.rb
@@ -109,7 +109,7 @@ RSpec.describe Gitlab::ExternalAuthorization::Client do
describe 'for non-ldap users with identities' do
before do
- %w(twitter facebook).each do |provider|
+ %w[twitter facebook].each do |provider|
create(:identity, provider: provider, extern_uid: "#{provider}_external_id", user: user)
end
end
diff --git a/spec/lib/gitlab/favicon_spec.rb b/spec/lib/gitlab/favicon_spec.rb
index 033fa5d1b42..62071293764 100644
--- a/spec/lib/gitlab/favicon_spec.rb
+++ b/spec/lib/gitlab/favicon_spec.rb
@@ -60,7 +60,7 @@ RSpec.describe Gitlab::Favicon, :request_store do
subject { described_class.available_status_names }
it 'returns the available status names' do
- expect(subject).to eq %w(
+ expect(subject).to eq %w[
favicon_status_canceled
favicon_status_created
favicon_status_failed
@@ -73,7 +73,7 @@ RSpec.describe Gitlab::Favicon, :request_store do
favicon_status_skipped
favicon_status_success
favicon_status_warning
- )
+ ]
end
end
end
diff --git a/spec/lib/gitlab/feature_categories_spec.rb b/spec/lib/gitlab/feature_categories_spec.rb
index a35166a4499..11ddd08c968 100644
--- a/spec/lib/gitlab/feature_categories_spec.rb
+++ b/spec/lib/gitlab/feature_categories_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe Gitlab::FeatureCategories do
- let(:fake_categories) { %w(foo bar) }
+ let(:fake_categories) { %w[foo bar] }
subject(:feature_categories) { described_class.new(fake_categories) }
diff --git a/spec/lib/gitlab/file_detector_spec.rb b/spec/lib/gitlab/file_detector_spec.rb
index 208acf28cc4..55bb1804d86 100644
--- a/spec/lib/gitlab/file_detector_spec.rb
+++ b/spec/lib/gitlab/file_detector_spec.rb
@@ -1,17 +1,17 @@
# frozen_string_literal: true
-require 'fast_spec_helper'
+require 'spec_helper'
RSpec.describe Gitlab::FileDetector do
describe '.types_in_paths' do
it 'returns the file types for the given paths' do
- expect(described_class.types_in_paths(%w(README.md CHANGELOG VERSION VERSION)))
- .to eq(%i{readme changelog version})
+ expect(described_class.types_in_paths(%w[README.md CHANGELOG VERSION VERSION]))
+ .to eq(%i[readme changelog version])
end
it 'does not include unrecognized file paths' do
- expect(described_class.types_in_paths(%w(README.md foo.txt)))
- .to eq(%i{readme})
+ expect(described_class.types_in_paths(%w[README.md foo.txt]))
+ .to eq(%i[readme])
end
end
@@ -25,7 +25,7 @@ RSpec.describe Gitlab::FileDetector do
extensions = ['txt', *Gitlab::MarkupHelper::EXTENSIONS]
extensions.each do |ext|
- %w(index readme).each do |file|
+ %w[index readme].each do |file|
expect(described_class.type_of("#{file}.#{ext}")).to eq(:readme)
end
end
@@ -45,13 +45,13 @@ RSpec.describe Gitlab::FileDetector do
end
it 'returns the type of a changelog file' do
- %w(CHANGELOG HISTORY CHANGES NEWS).each do |file|
+ %w[CHANGELOG HISTORY CHANGES NEWS].each do |file|
expect(described_class.type_of(file)).to eq(:changelog)
end
end
it 'returns the type of a license file' do
- %w(LICENSE LICENCE COPYING UNLICENSE UNLICENCE).each do |file|
+ %w[LICENSE LICENCE COPYING UNLICENSE UNLICENCE].each do |file|
expect(described_class.type_of(file)).to eq(:license)
end
end
@@ -73,7 +73,7 @@ RSpec.describe Gitlab::FileDetector do
end
it 'returns the type of an avatar' do
- %w(logo.gif logo.png logo.jpg).each do |file|
+ %w[logo.gif logo.png logo.jpg].each do |file|
expect(described_class.type_of(file)).to eq(:avatar)
end
end
diff --git a/spec/lib/gitlab/gfm/reference_rewriter_spec.rb b/spec/lib/gitlab/gfm/reference_rewriter_spec.rb
index 6de7cab9c42..75427ac0402 100644
--- a/spec/lib/gitlab/gfm/reference_rewriter_spec.rb
+++ b/spec/lib/gitlab/gfm/reference_rewriter_spec.rb
@@ -78,13 +78,13 @@ RSpec.describe Gitlab::Gfm::ReferenceRewriter do
context 'label referenced by id' do
let(:text) { '#1 and ~123' }
- it { is_expected.to eq %{#{old_project_ref}#1 and #{old_project_ref}~123} }
+ it { is_expected.to eq %(#{old_project_ref}#1 and #{old_project_ref}~123) }
end
context 'label referenced by text' do
let(:text) { '#1 and ~"test"' }
- it { is_expected.to eq %{#{old_project_ref}#1 and #{old_project_ref}~123} }
+ it { is_expected.to eq %(#{old_project_ref}#1 and #{old_project_ref}~123) }
end
end
@@ -99,13 +99,13 @@ RSpec.describe Gitlab::Gfm::ReferenceRewriter do
context 'label referenced by id' do
let(:text) { '#1 and ~321' }
- it { is_expected.to eq %{#{old_project_ref}#1 and #{old_project_ref}~321} }
+ it { is_expected.to eq %(#{old_project_ref}#1 and #{old_project_ref}~321) }
end
context 'label referenced by text' do
let(:text) { '#1 and ~"group label"' }
- it { is_expected.to eq %{#{old_project_ref}#1 and #{old_project_ref}~321} }
+ it { is_expected.to eq %(#{old_project_ref}#1 and #{old_project_ref}~321) }
end
end
end
@@ -149,7 +149,7 @@ RSpec.describe Gitlab::Gfm::ReferenceRewriter do
let(:text) { 'milestone: %"9.0"' }
- it { is_expected.to eq %[milestone: #{old_project_ref}%"9.0"] }
+ it { is_expected.to eq %(milestone: #{old_project_ref}%"9.0") }
end
context 'when referring to group milestone' do
diff --git a/spec/lib/gitlab/git/blame_spec.rb b/spec/lib/gitlab/git/blame_spec.rb
index 77361b09857..751611be5d2 100644
--- a/spec/lib/gitlab/git/blame_spec.rb
+++ b/spec/lib/gitlab/git/blame_spec.rb
@@ -48,6 +48,14 @@ RSpec.describe Gitlab::Git::Blame, feature_category: :source_code_management do
end
end
+ context 'when path is missing' do
+ let(:path) { 'unknown_file' }
+
+ it 'returns an empty array' do
+ expect(result).to eq([])
+ end
+ end
+
context "ISO-8859 encoding" do
let(:path) { 'encoding/iso8859.txt' }
diff --git a/spec/lib/gitlab/git/blob_spec.rb b/spec/lib/gitlab/git/blob_spec.rb
index 5bb4b84835d..59cf87ddc7e 100644
--- a/spec/lib/gitlab/git/blob_spec.rb
+++ b/spec/lib/gitlab/git/blob_spec.rb
@@ -154,18 +154,6 @@ RSpec.describe Gitlab::Git::Blob do
it_behaves_like '.find'
end
- describe '.find with Rugged enabled', :enable_rugged do
- it 'calls out to the Rugged implementation' do
- allow_next_instance_of(Rugged) do |instance|
- allow(instance).to receive(:rev_parse).with(TestEnv::BRANCH_SHA['master']).and_call_original
- end
-
- described_class.find(repository, TestEnv::BRANCH_SHA['master'], 'files/images/6049019_460s.jpg')
- end
-
- it_behaves_like '.find'
- end
-
describe '.raw' do
let(:raw_blob) { described_class.raw(repository, SeedRepo::RubyBlob::ID) }
let(:bad_blob) { described_class.raw(repository, SeedRepo::BigCommit::ID) }
diff --git a/spec/lib/gitlab/git/commit_spec.rb b/spec/lib/gitlab/git/commit_spec.rb
index 5c4be1003c3..d8d62ac9670 100644
--- a/spec/lib/gitlab/git/commit_spec.rb
+++ b/spec/lib/gitlab/git/commit_spec.rb
@@ -160,18 +160,6 @@ RSpec.describe Gitlab::Git::Commit, feature_category: :source_code_management do
it_behaves_like '.find'
end
- describe '.find with Rugged enabled', :enable_rugged do
- it 'calls out to the Rugged implementation' do
- allow_next_instance_of(Rugged) do |instance|
- allow(instance).to receive(:rev_parse).with(SeedRepo::Commit::ID).and_call_original
- end
-
- described_class.find(repository, SeedRepo::Commit::ID)
- end
-
- it_behaves_like '.find'
- end
-
describe '.last_for_path' do
context 'no path' do
subject { described_class.last_for_path(repository, 'master') }
@@ -459,18 +447,6 @@ RSpec.describe Gitlab::Git::Commit, feature_category: :source_code_management do
end
end
- describe '.batch_by_oid with Rugged enabled', :enable_rugged do
- it_behaves_like '.batch_by_oid'
-
- it 'calls out to the Rugged implementation' do
- allow_next_instance_of(Rugged) do |instance|
- allow(instance).to receive(:rev_parse).with(SeedRepo::Commit::ID).and_call_original
- end
-
- described_class.batch_by_oid(repository, [SeedRepo::Commit::ID])
- end
- end
-
describe '.extract_signature_lazily' do
subject { described_class.extract_signature_lazily(repository, commit_id).itself }
diff --git a/spec/lib/gitlab/git/merge_base_spec.rb b/spec/lib/gitlab/git/merge_base_spec.rb
index fda2232c2c3..cbe47aae852 100644
--- a/spec/lib/gitlab/git/merge_base_spec.rb
+++ b/spec/lib/gitlab/git/merge_base_spec.rb
@@ -11,13 +11,13 @@ RSpec.describe Gitlab::Git::MergeBase do
shared_context 'existing refs with a merge base', :existing_refs do
let(:refs) do
- %w(304d257dcb821665ab5110318fc58a007bd104ed 0031876facac3f2b2702a0e53a26e89939a42209)
+ %w[304d257dcb821665ab5110318fc58a007bd104ed 0031876facac3f2b2702a0e53a26e89939a42209]
end
end
shared_context 'when passing a missing ref', :missing_ref do
let(:refs) do
- %w(304d257dcb821665ab5110318fc58a007bd104ed aaaa)
+ %w[304d257dcb821665ab5110318fc58a007bd104ed aaaa]
end
end
@@ -51,13 +51,13 @@ RSpec.describe Gitlab::Git::MergeBase do
end
it 'returns a merge base when passing 2 branch names' do
- merge_base = described_class.new(repository, %w(master feature))
+ merge_base = described_class.new(repository, %w[master feature])
expect(merge_base.sha).to be_present
end
it 'returns a merge base when passing a tag name' do
- merge_base = described_class.new(repository, %w(master v1.0.0))
+ merge_base = described_class.new(repository, %w[master v1.0.0])
expect(merge_base.sha).to be_present
end
diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb
index 47b5986cfd8..5791d9c524f 100644
--- a/spec/lib/gitlab/git/repository_spec.rb
+++ b/spec/lib/gitlab/git/repository_spec.rb
@@ -2435,7 +2435,7 @@ RSpec.describe Gitlab::Git::Repository, feature_category: :source_code_managemen
end
it 'deletes all refs except those with the specified prefixes' do
- repository.delete_all_refs_except(%w(refs/keep refs/also-keep refs/heads))
+ repository.delete_all_refs_except(%w[refs/keep refs/also-keep refs/heads])
expect(repository.ref_exists?("refs/delete/a")).to be(false)
expect(repository.ref_exists?("refs/also-delete/b")).to be(false)
expect(repository.ref_exists?("refs/keep/c")).to be(true)
@@ -2722,15 +2722,15 @@ RSpec.describe Gitlab::Git::Repository, feature_category: :source_code_managemen
describe '#check_objects_exist' do
it 'returns hash specifying which object exists in repo' do
- refs_exist = %w(
+ refs_exist = %w[
b83d6e391c22777fca1ed3012fce84f633d7fed0
498214de67004b1da3d820901307bed2a68a8ef6
1b12f15a11fc6e62177bef08f47bc7b5ce50b141
- )
- refs_dont_exist = %w(
+ ]
+ refs_dont_exist = %w[
1111111111111111111111111111111111111111
2222222222222222222222222222222222222222
- )
+ ]
object_existence_map = repository.check_objects_exist(refs_exist + refs_dont_exist)
expect(object_existence_map).to eq({
'b83d6e391c22777fca1ed3012fce84f633d7fed0' => true,
diff --git a/spec/lib/gitlab/git/rugged_impl/use_rugged_spec.rb b/spec/lib/gitlab/git/rugged_impl/use_rugged_spec.rb
deleted file mode 100644
index d5a0ab3d5e0..00000000000
--- a/spec/lib/gitlab/git/rugged_impl/use_rugged_spec.rb
+++ /dev/null
@@ -1,116 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Gitlab::Git::RuggedImpl::UseRugged, feature_category: :gitaly do
- let(:project) { create(:project, :repository) }
- let(:repository) { project.repository }
- let(:feature_flag_name) { wrapper.rugged_feature_keys.first }
-
- subject(:wrapper) do
- klazz = Class.new do
- include Gitlab::Git::RuggedImpl::UseRugged
-
- def rugged_test(ref, test_number); end
- end
-
- klazz.new
- end
-
- describe '#execute_rugged_call', :request_store do
- let(:args) { ['refs/heads/master', 1] }
-
- before do
- allow(Gitlab::PerformanceBar).to receive(:enabled_for_request?).and_return(true)
- end
-
- it 'instruments Rugged call' do
- expect(subject).to receive(:rugged_test).with(args)
-
- subject.execute_rugged_call(:rugged_test, args)
-
- expect(Gitlab::RuggedInstrumentation.query_count).to eq(1)
- expect(Gitlab::RuggedInstrumentation.list_call_details.count).to eq(1)
- end
- end
-
- describe '#use_rugged?' do
- it 'returns false' do
- expect(subject.use_rugged?(repository, feature_flag_name)).to be false
- end
- end
-
- describe '#running_puma_with_multiple_threads?' do
- context 'when using Puma' do
- before do
- stub_const('::Puma', double('puma constant'))
- allow(Gitlab::Runtime).to receive(:puma?).and_return(true)
- end
-
- it "returns false when Puma doesn't support the cli_config method" do
- allow(::Puma).to receive(:respond_to?).with(:cli_config).and_return(false)
-
- expect(subject.running_puma_with_multiple_threads?).to be_falsey
- end
-
- it 'returns false for single thread Puma' do
- allow(::Puma).to receive_message_chain(:cli_config, :options).and_return(max_threads: 1)
-
- expect(subject.running_puma_with_multiple_threads?).to be false
- end
-
- it 'returns true for multi-threaded Puma' do
- allow(::Puma).to receive_message_chain(:cli_config, :options).and_return(max_threads: 2)
-
- expect(subject.running_puma_with_multiple_threads?).to be true
- end
- end
-
- context 'when not using Puma' do
- before do
- allow(Gitlab::Runtime).to receive(:puma?).and_return(false)
- end
-
- it 'returns false' do
- expect(subject.running_puma_with_multiple_threads?).to be false
- end
- end
- end
-
- describe '#rugged_enabled_through_feature_flag?' do
- subject { wrapper.send(:rugged_enabled_through_feature_flag?) }
-
- before do
- allow(Feature).to receive(:enabled?).with(:feature_key_1).and_return(true)
- allow(Feature).to receive(:enabled?).with(:feature_key_2).and_return(true)
- allow(Feature).to receive(:enabled?).with(:feature_key_3).and_return(false)
- allow(Feature).to receive(:enabled?).with(:feature_key_4).and_return(false)
-
- stub_const('Gitlab::Git::RuggedImpl::Repository::FEATURE_FLAGS', feature_keys)
- end
-
- context 'no feature keys given' do
- let(:feature_keys) { [] }
-
- it { is_expected.to be_falsey }
- end
-
- context 'all features are enabled' do
- let(:feature_keys) { [:feature_key_1, :feature_key_2] }
-
- it { is_expected.to be_falsey }
- end
-
- context 'all features are not enabled' do
- let(:feature_keys) { [:feature_key_3, :feature_key_4] }
-
- it { is_expected.to be_falsey }
- end
-
- context 'some feature is enabled' do
- let(:feature_keys) { [:feature_key_4, :feature_key_2] }
-
- it { is_expected.to be_falsey }
- end
- end
-end
diff --git a/spec/lib/gitlab/git/tree_spec.rb b/spec/lib/gitlab/git/tree_spec.rb
index 9675e48a77f..090f9af2620 100644
--- a/spec/lib/gitlab/git/tree_spec.rb
+++ b/spec/lib/gitlab/git/tree_spec.rb
@@ -192,122 +192,4 @@ RSpec.describe Gitlab::Git::Tree, feature_category: :source_code_management do
end
end
end
-
- describe '.where with Rugged enabled', :enable_rugged do
- it 'does not call to the Rugged implementation' do
- allow_next_instance_of(Rugged) do |instance|
- allow(instance).not_to receive(:lookup)
- end
-
- described_class.where(repository, SeedRepo::Commit::ID, 'files', false, false)
- end
-
- it_behaves_like 'repo' do
- describe 'Pagination' do
- context 'with restrictive limit' do
- let(:pagination_params) { { limit: 3, page_token: nil } }
-
- it 'returns limited paginated list of tree objects' do
- expect(entries.count).to eq(3)
- expect(cursor.next_cursor).to be_present
- end
- end
-
- context 'when limit is equal to number of entries' do
- let(:entries_count) { entries.count }
-
- it 'returns all entries with a cursor' do
- result, cursor = Gitlab::Git::Tree.where(repository, sha, path, recursive, skip_flat_paths, rescue_not_found, { limit: entries_count, page_token: nil })
-
- expect(cursor).to eq(Gitaly::PaginationCursor.new)
- expect(result.entries.count).to eq(entries_count)
- end
- end
-
- context 'when limit is 0' do
- let(:pagination_params) { { limit: 0, page_token: nil } }
-
- it 'returns empty result' do
- expect(entries).to eq([])
- expect(cursor).to be_nil
- end
- end
-
- context 'when limit is missing' do
- let(:pagination_params) { { limit: nil, page_token: nil } }
-
- it 'returns all entries' do
- expect(entries.count).to be < 20
- expect(cursor).to eq(Gitaly::PaginationCursor.new)
- end
- end
-
- context 'when limit is negative' do
- let(:entries_count) { entries.count }
-
- it 'returns all entries' do
- result, cursor = Gitlab::Git::Tree.where(repository, sha, path, recursive, skip_flat_paths, rescue_not_found, { limit: -1, page_token: nil })
-
- expect(result.count).to eq(entries_count)
- expect(cursor).to eq(Gitaly::PaginationCursor.new)
- end
-
- context 'when token is provided' do
- let(:pagination_params) { { limit: 1000, page_token: nil } }
- let(:token) { entries.second.id }
-
- it 'returns all entries after token' do
- result, cursor = Gitlab::Git::Tree.where(repository, sha, path, recursive, skip_flat_paths, rescue_not_found, { limit: -1, page_token: token })
-
- expect(result.count).to eq(entries.count - 2)
- expect(cursor).to eq(Gitaly::PaginationCursor.new)
- end
- end
- end
-
- context 'when token does not exist' do
- let(:pagination_params) { { limit: 5, page_token: 'aabbccdd' } }
-
- it 'raises a command error' do
- expect { entries }.to raise_error(Gitlab::Git::CommandError, /could not find starting OID: aabbccdd/)
- end
- end
-
- context 'when limit is bigger than number of entries' do
- let(:pagination_params) { { limit: 1000, page_token: nil } }
-
- it 'returns only available entries' do
- expect(entries.count).to be < 20
- expect(cursor).to eq(Gitaly::PaginationCursor.new)
- end
- end
-
- it 'returns all tree entries in specific order during cursor pagination' do
- collected_entries = []
- token = nil
-
- expected_entries = entries
-
- loop do
- result, cursor = Gitlab::Git::Tree.where(repository, sha, path, recursive, skip_flat_paths, rescue_not_found, { limit: 5, page_token: token })
-
- collected_entries += result.entries
- token = cursor&.next_cursor
-
- break if token.blank?
- end
-
- expect(collected_entries.map(&:path)).to match_array(expected_entries.map(&:path))
-
- expected_order = [
- collected_entries.select(&:dir?).map(&:path),
- collected_entries.select(&:file?).map(&:path),
- collected_entries.select(&:submodule?).map(&:path)
- ].flatten
-
- expect(collected_entries.map(&:path)).to eq(expected_order)
- end
- end
- end
- end
end
diff --git a/spec/lib/gitlab/git_access_spec.rb b/spec/lib/gitlab/git_access_spec.rb
index 1b205aa5c85..975e8bdd3ac 100644
--- a/spec/lib/gitlab/git_access_spec.rb
+++ b/spec/lib/gitlab/git_access_spec.rb
@@ -957,7 +957,7 @@ RSpec.describe Gitlab::GitAccess, :aggregate_failures, feature_category: :system
}
}
- [%w(feature exact), ['feat*', 'wildcard']].each do |protected_branch_name, protected_branch_type|
+ [%w[feature exact], ['feat*', 'wildcard']].each do |protected_branch_name, protected_branch_type|
context do
let(:who_can_action) { :maintainers_can_push }
let(:protected_branch) { create(:protected_branch, who_can_action, name: protected_branch_name, project: project) }
diff --git a/spec/lib/gitlab/git_audit_event_spec.rb b/spec/lib/gitlab/git_audit_event_spec.rb
deleted file mode 100644
index c533b39f550..00000000000
--- a/spec/lib/gitlab/git_audit_event_spec.rb
+++ /dev/null
@@ -1,79 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Gitlab::GitAuditEvent, feature_category: :source_code_management do
- let_it_be(:player) { create(:user) }
- let_it_be(:group) { create(:group, :public) }
- let_it_be(:project) { create(:project) }
-
- subject { described_class.new(player, project) }
-
- describe '#send_audit_event' do
- let(:msg) { 'valid_msg' }
-
- context 'with successfully sending' do
- let_it_be(:project) { create(:project, namespace: group) }
-
- before do
- allow(::Gitlab::Audit::Auditor).to receive(:audit)
- end
-
- context 'when player is a regular user' do
- it 'sends git audit event' do
- expect(::Gitlab::Audit::Auditor).to receive(:audit).with(a_hash_including(
- name: 'repository_git_operation',
- stream_only: true,
- author: player,
- scope: project,
- target: project,
- message: msg
- )).once
-
- subject.send_audit_event(msg)
- end
- end
-
- context 'when player is ::API::Support::GitAccessActor' do
- let_it_be(:user) { player }
- let_it_be(:key) { create(:key, user: user) }
- let_it_be(:git_access_actor) { ::API::Support::GitAccessActor.new(user: user, key: key) }
-
- subject { described_class.new(git_access_actor, project) }
-
- it 'sends git audit event' do
- expect(::Gitlab::Audit::Auditor).to receive(:audit).with(a_hash_including(
- name: 'repository_git_operation',
- stream_only: true,
- author: git_access_actor.deploy_key_or_user,
- scope: project,
- target: project,
- message: msg
- )).once
-
- subject.send_audit_event(msg)
- end
- end
- end
-
- context 'when user is blank' do
- let_it_be(:player) { nil }
-
- it 'does not send git audit event' do
- expect(::Gitlab::Audit::Auditor).not_to receive(:audit)
-
- subject.send_audit_event(msg)
- end
- end
-
- context 'when project is blank' do
- let_it_be(:project) { nil }
-
- it 'does not send git audit event' do
- expect(::Gitlab::Audit::Auditor).not_to receive(:audit)
-
- subject.send_audit_event(msg)
- end
- end
- end
-end
diff --git a/spec/lib/gitlab/gitaly_client/commit_service_spec.rb b/spec/lib/gitlab/gitaly_client/commit_service_spec.rb
index 2ee9d85c723..02c7abadd99 100644
--- a/spec/lib/gitlab/gitaly_client/commit_service_spec.rb
+++ b/spec/lib/gitlab/gitaly_client/commit_service_spec.rb
@@ -1041,9 +1041,11 @@ RSpec.describe Gitlab::GitalyClient::CommitService, feature_category: :gitaly do
end
describe '#raw_blame' do
- let(:project) { create(:project, :test_repo) }
+ let_it_be(:project) { create(:project, :test_repo) }
+
let(:revision) { 'blame-on-renamed' }
let(:path) { 'files/plain_text/renamed' }
+ let(:range) { nil }
let(:blame_headers) do
[
@@ -1073,6 +1075,31 @@ RSpec.describe Gitlab::GitalyClient::CommitService, feature_category: :gitaly do
is_expected.not_to include(blame_headers[0], blame_headers[1], blame_headers[4])
end
end
+
+ context 'when out of range' do
+ let(:range) { '9999,99999' }
+
+ it { expect { blame }.to raise_error(ArgumentError, 'range is outside of the file length') }
+ end
+
+ context 'when a file path is not found' do
+ let(:path) { 'unknown/path' }
+
+ it { expect { blame }.to raise_error(ArgumentError, 'path not found in revision') }
+ end
+
+ context 'when an unknown exception is raised' do
+ let(:gitaly_exception) { GRPC::BadStatus.new(GRPC::Core::StatusCodes::NOT_FOUND) }
+
+ before do
+ expect_any_instance_of(Gitaly::CommitService::Stub)
+ .to receive(:raw_blame)
+ .with(gitaly_request_with_path(storage_name, relative_path), kind_of(Hash))
+ .and_raise(gitaly_exception)
+ end
+
+ it { expect { blame }.to raise_error(gitaly_exception) }
+ end
end
describe '#get_commit_signatures' do
diff --git a/spec/lib/gitlab/gitaly_client/conflict_files_stitcher_spec.rb b/spec/lib/gitlab/gitaly_client/conflict_files_stitcher_spec.rb
index d0787d8b673..816b59b96ae 100644
--- a/spec/lib/gitlab/gitaly_client/conflict_files_stitcher_spec.rb
+++ b/spec/lib/gitlab/gitaly_client/conflict_files_stitcher_spec.rb
@@ -3,12 +3,12 @@
require 'spec_helper'
RSpec.describe Gitlab::GitalyClient::ConflictFilesStitcher do
+ let_it_be(:target_project) { create(:project, :repository) }
+ let_it_be(:target_repository) { target_project.repository.raw }
+ let_it_be(:target_gitaly_repository) { target_repository.gitaly_repository }
+
describe 'enumeration' do
it 'combines segregated ConflictFile messages together' do
- target_project = create(:project, :repository)
- target_repository = target_project.repository.raw
- target_gitaly_repository = target_repository.gitaly_repository
-
ancestor_path_1 = 'ancestor/path/1'
our_path_1 = 'our/path/1'
their_path_1 = 'their/path/1'
@@ -69,5 +69,49 @@ RSpec.describe Gitlab::GitalyClient::ConflictFilesStitcher do
expect(conflict_files[1].repository).to eq(target_repository)
expect(conflict_files[1].commit_oid).to eq(commit_oid_2)
end
+
+ it 'handles non-latin character names' do
+ ancestor_path_1_utf8 = "ancestor/テスト.txt"
+ our_path_1_utf8 = "our/テスト.txt"
+ their_path_1_utf8 = "their/テスト.txt"
+
+ ancestor_path_1 = String.new('ancestor/テスト.txt', encoding: Encoding::US_ASCII)
+ our_path_1 = String.new('our/テスト.txt', encoding: Encoding::US_ASCII)
+ their_path_1 = String.new('their/テスト.txt', encoding: Encoding::US_ASCII)
+ our_mode_1 = 0744
+ commit_oid_1 = 'f00'
+ content_1 = 'content of the first file'
+
+ header_1 = double(
+ repository: target_gitaly_repository,
+ commit_oid: commit_oid_1,
+ ancestor_path: ancestor_path_1.dup,
+ our_path: our_path_1.dup,
+ their_path: their_path_1.dup,
+ our_mode: our_mode_1
+ )
+
+ messages = [
+ double(files: [double(header: header_1), double(header: nil, content: content_1[0..5])]),
+ double(files: [double(header: nil, content: content_1[6..])])
+ ]
+
+ conflict_files = described_class.new(messages, target_repository.gitaly_repository).to_a
+
+ expect(conflict_files.size).to be(1)
+
+ expect(conflict_files[0].content).to eq(content_1)
+ expect(conflict_files[0].ancestor_path).to eq(ancestor_path_1_utf8)
+ expect(conflict_files[0].their_path).to eq(their_path_1_utf8)
+ expect(conflict_files[0].our_path).to eq(our_path_1_utf8)
+ expect(conflict_files[0].our_mode).to be(our_mode_1)
+ expect(conflict_files[0].repository).to eq(target_repository)
+ expect(conflict_files[0].commit_oid).to eq(commit_oid_1)
+
+ # Doesn't equal the ASCII version
+ expect(conflict_files[0].ancestor_path).not_to eq(ancestor_path_1)
+ expect(conflict_files[0].their_path).not_to eq(their_path_1)
+ expect(conflict_files[0].our_path).not_to eq(our_path_1)
+ end
end
end
diff --git a/spec/lib/gitlab/gitaly_client/operation_service_spec.rb b/spec/lib/gitlab/gitaly_client/operation_service_spec.rb
index bd0341d51bf..f50675fee60 100644
--- a/spec/lib/gitlab/gitaly_client/operation_service_spec.rb
+++ b/spec/lib/gitlab/gitaly_client/operation_service_spec.rb
@@ -1184,7 +1184,7 @@ RSpec.describe Gitlab::GitalyClient::OperationService, feature_category: :source
patch_names.map { |name| File.read(File.join(patches_folder, name)) }.join("\n")
end
- let(:patch_names) { %w(0001-This-does-not-apply-to-the-feature-branch.patch) }
+ let(:patch_names) { %w[0001-This-does-not-apply-to-the-feature-branch.patch] }
let(:branch_name) { 'branch-with-patches' }
subject(:commit_patches) do
@@ -1203,7 +1203,7 @@ RSpec.describe Gitlab::GitalyClient::OperationService, feature_category: :source
end
context 'when the patch could not be applied' do
- let(:patch_names) { %w(0001-This-does-not-apply-to-the-feature-branch.patch) }
+ let(:patch_names) { %w[0001-This-does-not-apply-to-the-feature-branch.patch] }
let(:branch_name) { 'feature' }
it 'raises the correct error' do
diff --git a/spec/lib/gitlab/gitaly_client/ref_service_spec.rb b/spec/lib/gitlab/gitaly_client/ref_service_spec.rb
index ae9276cf90b..118b316f2d4 100644
--- a/spec/lib/gitlab/gitaly_client/ref_service_spec.rb
+++ b/spec/lib/gitlab/gitaly_client/ref_service_spec.rb
@@ -67,7 +67,7 @@ RSpec.describe Gitlab::GitalyClient::RefService, feature_category: :gitaly do
.with(gitaly_request_with_params(merged_only: true, merged_branches: ['test']), kind_of(Hash))
.and_return([])
- client.merged_branches(%w(test))
+ client.merged_branches(%w[test])
end
end
@@ -425,7 +425,7 @@ RSpec.describe Gitlab::GitalyClient::RefService, feature_category: :gitaly do
end
describe '#delete_refs' do
- let(:prefixes) { %w(refs/heads refs/keep-around) }
+ let(:prefixes) { %w[refs/heads refs/keep-around] }
subject(:delete_refs) { client.delete_refs(except_with_prefixes: prefixes) }
diff --git a/spec/lib/gitlab/gitaly_client/repository_service_spec.rb b/spec/lib/gitlab/gitaly_client/repository_service_spec.rb
index 283a9cb45dc..727bf494ee6 100644
--- a/spec/lib/gitlab/gitaly_client/repository_service_spec.rb
+++ b/spec/lib/gitlab/gitaly_client/repository_service_spec.rb
@@ -140,6 +140,44 @@ RSpec.describe Gitlab::GitalyClient::RepositoryService, feature_category: :gital
end
end
+ describe '#fork_repository' do
+ let(:source_repository) { Gitlab::Git::Repository.new('default', 'repo/path', '', 'group/project') }
+
+ context 'when branch is not provided' do
+ it 'sends a create_fork message' do
+ expected_request = gitaly_request_with_params(
+ source_repository: source_repository.gitaly_repository,
+ revision: ""
+ )
+
+ expect_any_instance_of(Gitaly::RepositoryService::Stub)
+ .to receive(:create_fork)
+ .with(expected_request, kind_of(Hash))
+ .and_return(double(value: true))
+
+ client.fork_repository(source_repository)
+ end
+ end
+
+ context 'when branch is provided' do
+ it 'sends a create_fork message including revision' do
+ branch = 'wip'
+
+ expected_request = gitaly_request_with_params(
+ source_repository: source_repository.gitaly_repository,
+ revision: "refs/heads/#{branch}"
+ )
+
+ expect_any_instance_of(Gitaly::RepositoryService::Stub)
+ .to receive(:create_fork)
+ .with(expected_request, kind_of(Hash))
+ .and_return(double(value: true))
+
+ client.fork_repository(source_repository, branch)
+ end
+ end
+ end
+
describe '#import_repository' do
let(:source) { 'https://example.com/git/repo.git' }
diff --git a/spec/lib/gitlab/gitaly_client/storage_settings_spec.rb b/spec/lib/gitlab/gitaly_client/storage_settings_spec.rb
index 0c4c8de52ae..7252f7d6afb 100644
--- a/spec/lib/gitlab/gitaly_client/storage_settings_spec.rb
+++ b/spec/lib/gitlab/gitaly_client/storage_settings_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::GitalyClient::StorageSettings do
+RSpec.describe Gitlab::GitalyClient::StorageSettings, feature_category: :gitaly do
describe "#initialize" do
context 'when the storage contains no path' do
it 'raises an error' do
@@ -62,16 +62,16 @@ RSpec.describe Gitlab::GitalyClient::StorageSettings do
end
describe '.disk_access_denied?' do
- context 'when Rugged is enabled', :enable_rugged do
- it 'returns false' do
- expect(described_class.disk_access_denied?).to be_falsey
- end
- end
+ subject { described_class.disk_access_denied? }
- context 'when Rugged is disabled' do
- it 'returns true' do
- expect(described_class.disk_access_denied?).to be_truthy
+ it { is_expected.to be_truthy }
+
+ context 'in case of an exception' do
+ before do
+ allow(described_class).to receive(:temporarily_allowed?).and_raise('boom')
end
+
+ it { is_expected.to be_falsey }
end
end
end
diff --git a/spec/lib/gitlab/gitaly_client_spec.rb b/spec/lib/gitlab/gitaly_client_spec.rb
index 0073d2ebe80..00639d9574b 100644
--- a/spec/lib/gitlab/gitaly_client_spec.rb
+++ b/spec/lib/gitlab/gitaly_client_spec.rb
@@ -517,6 +517,44 @@ RSpec.describe Gitlab::GitalyClient, feature_category: :gitaly do
end
end
+ describe '.fetch_relative_path' do
+ subject { described_class.request_kwargs('default', timeout: 1)[:metadata]['relative-path-bin'] }
+
+ let(:relative_path) { 'relative_path' }
+
+ context 'when RequestStore is disabled' do
+ it 'does not set a relative path' do
+ is_expected.to be_nil
+ end
+ end
+
+ context 'when RequestStore is enabled', :request_store do
+ context 'when RequestStore is empty' do
+ it 'does not set a relative path' do
+ is_expected.to be_nil
+ end
+ end
+
+ context 'when RequestStore contains a relalive_path value' do
+ before do
+ Gitlab::SafeRequestStore[:gitlab_git_relative_path] = relative_path
+ end
+
+ it 'sets a base64 encoded version of relative_path' do
+ is_expected.to eq(relative_path)
+ end
+
+ context 'when relalive_path is empty' do
+ let(:relative_path) { '' }
+
+ it 'does not set a relative path' do
+ is_expected.to be_nil
+ end
+ end
+ end
+ end
+ end
+
context 'gitlab_git_env' do
let(:policy) { 'gitaly-route-repository-accessor-policy' }
diff --git a/spec/lib/gitlab/github_import/attachments_downloader_spec.rb b/spec/lib/gitlab/github_import/attachments_downloader_spec.rb
index 72d8a9c0403..65c5a7daeb2 100644
--- a/spec/lib/gitlab/github_import/attachments_downloader_spec.rb
+++ b/spec/lib/gitlab/github_import/attachments_downloader_spec.rb
@@ -94,9 +94,9 @@ RSpec.describe Gitlab::GithubImport::AttachmentsDownloader, feature_category: :i
end
end
- context 'when attachment is behind a redirect' do
- let_it_be(:file_url) { "https://github.com/test/project/assets/142635249/4b9f9c90-f060-4845-97cf-b24c558bcb11" }
- let(:redirect_url) { "https://https://github-production-user-asset-6210df.s3.amazonaws.com/142635249/740edb05293e.jpg" }
+ context 'when attachment is behind a github asset endpoint' do
+ let(:file_url) { "https://github.com/test/project/assets/142635249/4b9f9c90-f060-4845-97cf-b24c558bcb11" }
+ let(:redirect_url) { "https://github-production-user-asset-6210df.s3.amazonaws.com/142635249/740edb05293e.jpg" }
let(:sample_response) do
instance_double(HTTParty::Response, redirection?: true, headers: { location: redirect_url })
end
@@ -115,6 +115,8 @@ RSpec.describe Gitlab::GithubImport::AttachmentsDownloader, feature_category: :i
end
context 'when url is not a redirection' do
+ let(:file_url) { "https://github.com/test/project/assets/142635249/4b9f9c90-f060-4845-97cf-b24c558bcb11.jpg" }
+
let(:sample_response) do
instance_double(HTTParty::Response, code: 200, redirection?: false)
end
@@ -125,8 +127,13 @@ RSpec.describe Gitlab::GithubImport::AttachmentsDownloader, feature_category: :i
.and_return sample_response
end
- it 'raises upon unsuccessful redirection' do
- expect { downloader.perform }.to raise_error("expected a redirect response, got #{sample_response.code}")
+ it 'queries with original file_url' do
+ expect(Gitlab::HTTP).to receive(:perform_request)
+ .with(Net::HTTP::Get, file_url, stream_body: true).and_yield(chunk_double)
+
+ file = downloader.perform
+
+ expect(File.exist?(file.path)).to eq(true)
end
end
diff --git a/spec/lib/gitlab/github_import/client_spec.rb b/spec/lib/gitlab/github_import/client_spec.rb
index 5f321a15de9..c409ec6983f 100644
--- a/spec/lib/gitlab/github_import/client_spec.rb
+++ b/spec/lib/gitlab/github_import/client_spec.rb
@@ -278,7 +278,7 @@ RSpec.describe Gitlab::GithubImport::Client, feature_category: :importers do
client.with_rate_limit do
if retries == 0
retries += 1
- raise(Octokit::TooManyRequests)
+ raise(Octokit::TooManyRequests.new(body: 'primary'))
end
end
@@ -306,6 +306,37 @@ RSpec.describe Gitlab::GithubImport::Client, feature_category: :importers do
expect(client.with_rate_limit { 10 }).to eq(10)
end
+ context 'when threshold is hit' do
+ it 'raises a RateLimitError with the appropriate message' do
+ expect(client).to receive(:requests_remaining?).and_return(false)
+
+ expect { client.with_rate_limit }
+ .to raise_error(Gitlab::GithubImport::RateLimitError, 'Internal threshold reached')
+ end
+ end
+
+ context 'when primary rate limit hit' do
+ let(:limited_block) { -> { raise(Octokit::TooManyRequests.new(body: 'primary')) } }
+
+ it 're-raises a RateLimitError with the appropriate message' do
+ expect(client).to receive(:requests_remaining?).and_return(true)
+
+ expect { client.with_rate_limit(&limited_block) }
+ .to raise_error(Gitlab::GithubImport::RateLimitError, 'primary')
+ end
+ end
+
+ context 'when secondary rate limit hit' do
+ let(:limited_block) { -> { raise(Octokit::TooManyRequests.new(body: 'secondary')) } }
+
+ it 're-raises a RateLimitError with the appropriate message' do
+ expect(client).to receive(:requests_remaining?).and_return(true)
+
+ expect { client.with_rate_limit(&limited_block) }
+ .to raise_error(Gitlab::GithubImport::RateLimitError, 'secondary')
+ end
+ end
+
context 'when Faraday error received from octokit', :aggregate_failures do
let(:error_class) { described_class::CLIENT_CONNECTION_ERROR }
let(:info_params) { { 'error.class': error_class } }
@@ -392,7 +423,7 @@ RSpec.describe Gitlab::GithubImport::Client, feature_category: :importers do
describe '#raise_or_wait_for_rate_limit' do
context 'when running in parallel mode' do
it 'raises RateLimitError' do
- expect { client.raise_or_wait_for_rate_limit }
+ expect { client.raise_or_wait_for_rate_limit('primary') }
.to raise_error(Gitlab::GithubImport::RateLimitError)
end
end
@@ -404,7 +435,7 @@ RSpec.describe Gitlab::GithubImport::Client, feature_category: :importers do
expect(client).to receive(:rate_limit_resets_in).and_return(1)
expect(client).to receive(:sleep).with(1)
- client.raise_or_wait_for_rate_limit
+ client.raise_or_wait_for_rate_limit('primary')
end
it 'increments the rate limit counter' do
@@ -420,7 +451,7 @@ RSpec.describe Gitlab::GithubImport::Client, feature_category: :importers do
.to receive(:increment)
.and_call_original
- client.raise_or_wait_for_rate_limit
+ client.raise_or_wait_for_rate_limit('primary')
end
end
end
diff --git a/spec/lib/gitlab/github_import/importer/collaborators_importer_spec.rb b/spec/lib/gitlab/github_import/importer/collaborators_importer_spec.rb
index dcb02f32a28..6f602531d23 100644
--- a/spec/lib/gitlab/github_import/importer/collaborators_importer_spec.rb
+++ b/spec/lib/gitlab/github_import/importer/collaborators_importer_spec.rb
@@ -82,7 +82,7 @@ RSpec.describe Gitlab::GithubImport::Importer::CollaboratorsImporter, feature_ca
it 'imports each collaborator in parallel' do
expect(Gitlab::GithubImport::ImportCollaboratorWorker).to receive(:perform_in)
- .with(1.second, project.id, an_instance_of(Hash), an_instance_of(String))
+ .with(1, project.id, an_instance_of(Hash), an_instance_of(String))
waiter = importer.parallel_import
diff --git a/spec/lib/gitlab/github_import/importer/diff_notes_importer_spec.rb b/spec/lib/gitlab/github_import/importer/diff_notes_importer_spec.rb
index 945b742b025..4e8066ecb69 100644
--- a/spec/lib/gitlab/github_import/importer/diff_notes_importer_spec.rb
+++ b/spec/lib/gitlab/github_import/importer/diff_notes_importer_spec.rb
@@ -98,7 +98,7 @@ RSpec.describe Gitlab::GithubImport::Importer::DiffNotesImporter, feature_catego
.and_yield(github_comment)
expect(Gitlab::GithubImport::ImportDiffNoteWorker).to receive(:perform_in)
- .with(1.second, project.id, an_instance_of(Hash), an_instance_of(String))
+ .with(1, project.id, an_instance_of(Hash), an_instance_of(String))
waiter = importer.parallel_import
diff --git a/spec/lib/gitlab/github_import/importer/issue_events_importer_spec.rb b/spec/lib/gitlab/github_import/importer/issue_events_importer_spec.rb
index 04b694dc0cb..9aba6a2b02c 100644
--- a/spec/lib/gitlab/github_import/importer/issue_events_importer_spec.rb
+++ b/spec/lib/gitlab/github_import/importer/issue_events_importer_spec.rb
@@ -78,7 +78,7 @@ RSpec.describe Gitlab::GithubImport::Importer::IssueEventsImporter, feature_cate
allow(importer).to receive(:each_object_to_import).and_yield(issue_event)
expect(Gitlab::GithubImport::ImportIssueEventWorker).to receive(:perform_in).with(
- 1.second, project.id, an_instance_of(Hash), an_instance_of(String)
+ 1, project.id, an_instance_of(Hash), an_instance_of(String)
)
waiter = importer.parallel_import
diff --git a/spec/lib/gitlab/github_import/importer/issues_importer_spec.rb b/spec/lib/gitlab/github_import/importer/issues_importer_spec.rb
index d6fd1a4739c..1bfdce04187 100644
--- a/spec/lib/gitlab/github_import/importer/issues_importer_spec.rb
+++ b/spec/lib/gitlab/github_import/importer/issues_importer_spec.rb
@@ -92,7 +92,7 @@ RSpec.describe Gitlab::GithubImport::Importer::IssuesImporter, feature_category:
expect(Gitlab::GithubImport::ImportIssueWorker)
.to receive(:perform_in)
- .with(1.second, project.id, an_instance_of(Hash), an_instance_of(String))
+ .with(1, project.id, an_instance_of(Hash), an_instance_of(String))
waiter = importer.parallel_import
diff --git a/spec/lib/gitlab/github_import/importer/lfs_objects_importer_spec.rb b/spec/lib/gitlab/github_import/importer/lfs_objects_importer_spec.rb
index fab9d26532d..3f5ee68d264 100644
--- a/spec/lib/gitlab/github_import/importer/lfs_objects_importer_spec.rb
+++ b/spec/lib/gitlab/github_import/importer/lfs_objects_importer_spec.rb
@@ -119,7 +119,7 @@ RSpec.describe Gitlab::GithubImport::Importer::LfsObjectsImporter, feature_categ
end
expect(Gitlab::GithubImport::ImportLfsObjectWorker).to receive(:perform_in)
- .with(1.second, project.id, an_instance_of(Hash), an_instance_of(String))
+ .with(1, project.id, an_instance_of(Hash), an_instance_of(String))
waiter = importer.parallel_import
diff --git a/spec/lib/gitlab/github_import/importer/note_importer_spec.rb b/spec/lib/gitlab/github_import/importer/note_importer_spec.rb
index 91311a8e90f..b5fe8c207c8 100644
--- a/spec/lib/gitlab/github_import/importer/note_importer_spec.rb
+++ b/spec/lib/gitlab/github_import/importer/note_importer_spec.rb
@@ -99,7 +99,7 @@ RSpec.describe Gitlab::GithubImport::Importer::NoteImporter, feature_category: :
end
context 'when the note have invalid chars' do
- let(:note_body) { %{There were an invalid char "\u0000" <= right here} }
+ let(:note_body) { %(There were an invalid char "\u0000" <= right here) }
it 'removes invalid chars' do
expect(importer.user_finder)
diff --git a/spec/lib/gitlab/github_import/importer/notes_importer_spec.rb b/spec/lib/gitlab/github_import/importer/notes_importer_spec.rb
index 841cc8178ea..8c93963f325 100644
--- a/spec/lib/gitlab/github_import/importer/notes_importer_spec.rb
+++ b/spec/lib/gitlab/github_import/importer/notes_importer_spec.rb
@@ -84,7 +84,7 @@ RSpec.describe Gitlab::GithubImport::Importer::NotesImporter, feature_category:
.and_yield(github_comment)
expect(Gitlab::GithubImport::ImportNoteWorker).to receive(:perform_in)
- .with(1.second, project.id, an_instance_of(Hash), an_instance_of(String))
+ .with(1, project.id, an_instance_of(Hash), an_instance_of(String))
waiter = importer.parallel_import
diff --git a/spec/lib/gitlab/github_import/importer/protected_branches_importer_spec.rb b/spec/lib/gitlab/github_import/importer/protected_branches_importer_spec.rb
index 6a8b14a2690..8e99585109b 100644
--- a/spec/lib/gitlab/github_import/importer/protected_branches_importer_spec.rb
+++ b/spec/lib/gitlab/github_import/importer/protected_branches_importer_spec.rb
@@ -144,7 +144,7 @@ RSpec.describe Gitlab::GithubImport::Importer::ProtectedBranchesImporter, featur
it 'imports each protected branch in parallel' do
expect(Gitlab::GithubImport::ImportProtectedBranchWorker)
.to receive(:perform_in)
- .with(1.second, project.id, an_instance_of(Hash), an_instance_of(String))
+ .with(1, project.id, an_instance_of(Hash), an_instance_of(String))
expect(Gitlab::GithubImport::ObjectCounter)
.to receive(:increment).with(project, :protected_branch, :fetched)
diff --git a/spec/lib/gitlab/github_import/importer/pull_requests/review_requests_importer_spec.rb b/spec/lib/gitlab/github_import/importer/pull_requests/review_requests_importer_spec.rb
index d0145ba1120..1977815e3a0 100644
--- a/spec/lib/gitlab/github_import/importer/pull_requests/review_requests_importer_spec.rb
+++ b/spec/lib/gitlab/github_import/importer/pull_requests/review_requests_importer_spec.rb
@@ -97,7 +97,7 @@ RSpec.describe Gitlab::GithubImport::Importer::PullRequests::ReviewRequestsImpor
{ id: 4, login: 'alice' },
{ id: 5, login: 'bob' }
]
- },
+ }.deep_stringify_keys,
instance_of(String)
],
[
@@ -108,7 +108,7 @@ RSpec.describe Gitlab::GithubImport::Importer::PullRequests::ReviewRequestsImpor
users: [
{ id: 4, login: 'alice' }
]
- },
+ }.deep_stringify_keys,
instance_of(String)
]
]
@@ -116,10 +116,10 @@ RSpec.describe Gitlab::GithubImport::Importer::PullRequests::ReviewRequestsImpor
it 'schedule import for each merge request reviewers' do
expect(Gitlab::GithubImport::PullRequests::ImportReviewRequestWorker)
- .to receive(:perform_in).with(1.second, *expected_worker_payload.first)
+ .to receive(:perform_in).with(1, *expected_worker_payload.first)
expect(Gitlab::GithubImport::PullRequests::ImportReviewRequestWorker)
- .to receive(:perform_in).with(1.second, *expected_worker_payload.second)
+ .to receive(:perform_in).with(1, *expected_worker_payload.second)
expect(Gitlab::GithubImport::ObjectCounter)
.to receive(:increment).twice.with(project, :pull_request_review_request, :fetched)
@@ -137,7 +137,7 @@ RSpec.describe Gitlab::GithubImport::Importer::PullRequests::ReviewRequestsImpor
it "doesn't schedule import this merge request reviewers" do
expect(Gitlab::GithubImport::PullRequests::ImportReviewRequestWorker)
- .to receive(:perform_in).with(1.second, *expected_worker_payload.second)
+ .to receive(:perform_in).with(1, *expected_worker_payload.second)
expect(Gitlab::GithubImport::ObjectCounter)
.to receive(:increment).once.with(project, :pull_request_review_request, :fetched)
diff --git a/spec/lib/gitlab/github_import/importer/pull_requests_importer_spec.rb b/spec/lib/gitlab/github_import/importer/pull_requests_importer_spec.rb
index cfd75fba849..10e413fdfe5 100644
--- a/spec/lib/gitlab/github_import/importer/pull_requests_importer_spec.rb
+++ b/spec/lib/gitlab/github_import/importer/pull_requests_importer_spec.rb
@@ -102,7 +102,7 @@ RSpec.describe Gitlab::GithubImport::Importer::PullRequestsImporter, feature_cat
expect(Gitlab::GithubImport::ImportPullRequestWorker)
.to receive(:perform_in)
- .with(1.second, project.id, an_instance_of(Hash), an_instance_of(String))
+ .with(1, project.id, an_instance_of(Hash), an_instance_of(String))
waiter = importer.parallel_import
diff --git a/spec/lib/gitlab/github_import/issuable_finder_spec.rb b/spec/lib/gitlab/github_import/issuable_finder_spec.rb
index d3236994cef..977fef95d64 100644
--- a/spec/lib/gitlab/github_import/issuable_finder_spec.rb
+++ b/spec/lib/gitlab/github_import/issuable_finder_spec.rb
@@ -2,40 +2,80 @@
require 'spec_helper'
-RSpec.describe Gitlab::GithubImport::IssuableFinder, :clean_gitlab_redis_cache do
- let(:project) { double(:project, id: 4, import_data: import_data) }
+RSpec.describe Gitlab::GithubImport::IssuableFinder, :clean_gitlab_redis_cache, feature_category: :importers do
+ let(:project) { build(:project, id: 20, import_data_attributes: import_data_attributes) }
let(:single_endpoint_optional_stage) { false }
- let(:import_data) do
- instance_double(
- ProjectImportData,
+ let(:import_data_attributes) do
+ {
data: {
optional_stages: {
single_endpoint_notes_import: single_endpoint_optional_stage
}
- }.deep_stringify_keys
- )
+ }
+ }
end
- let(:issue) { double(:issue, issuable_type: MergeRequest, issuable_id: 1) }
+ let(:merge_request) { create(:merge_request, source_project: project) }
+ let(:issue) { double(:issue, issuable_type: 'MergeRequest', issuable_id: merge_request.iid) }
let(:finder) { described_class.new(project, issue) }
describe '#database_id' do
- it 'returns nil when no cache is in place' do
- expect(finder.database_id).to be_nil
+ it 'returns nil if object does not exist' do
+ missing_issue = double(:issue, issuable_type: 'MergeRequest', issuable_id: 999)
+
+ expect(described_class.new(project, missing_issue).database_id).to be_nil
+ end
+
+ it 'fetches object id from database if not in cache' do
+ expect(finder.database_id).to eq(merge_request.id)
end
- it 'returns the ID of an issuable when the cache is in place' do
+ it 'fetches object id from cache if present' do
finder.cache_database_id(10)
expect(finder.database_id).to eq(10)
end
+ it 'returns nil and skips database read if cache has no record' do
+ finder.cache_database_id(-1)
+
+ expect(finder.database_id).to be_nil
+ end
+
it 'raises TypeError when the object is not supported' do
finder = described_class.new(project, double(:issue))
expect { finder.database_id }.to raise_error(TypeError)
end
+ context 'with FF import_fallback_to_db_empty_cache disabled' do
+ before do
+ stub_feature_flags(import_fallback_to_db_empty_cache: false)
+ end
+
+ it 'returns nil if object does not exist' do
+ missing_issue = double(:issue, issuable_type: 'MergeRequest', issuable_id: 999)
+
+ expect(described_class.new(project, missing_issue).database_id).to be_nil
+ end
+
+ it 'does not fetch object id from database if not in cache' do
+ expect(finder.database_id).to eq(nil)
+ end
+
+ it 'fetches object id from cache if present' do
+ finder.cache_database_id(10)
+
+ expect(finder.database_id).to eq(10)
+ end
+
+ it 'returns -1 if cache is -1' do
+ finder.cache_database_id(-1)
+
+ expect(finder.database_id).to eq(-1)
+ end
+ end
+
context 'when group is present' do
context 'when settings single_endpoint_notes_import is enabled' do
let(:single_endpoint_optional_stage) { true }
@@ -65,7 +105,7 @@ RSpec.describe Gitlab::GithubImport::IssuableFinder, :clean_gitlab_redis_cache d
it 'caches the ID of a database row' do
expect(Gitlab::Cache::Import::Caching)
.to receive(:write)
- .with('github-import/issuable-finder/4/MergeRequest/1', 10, timeout: 86400)
+ .with("github-import/issuable-finder/20/MergeRequest/#{merge_request.iid}", 10, timeout: 86400)
finder.cache_database_id(10)
end
diff --git a/spec/lib/gitlab/github_import/label_finder_spec.rb b/spec/lib/gitlab/github_import/label_finder_spec.rb
index 9905fce2a20..e46595974d1 100644
--- a/spec/lib/gitlab/github_import/label_finder_spec.rb
+++ b/spec/lib/gitlab/github_import/label_finder_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::GithubImport::LabelFinder, :clean_gitlab_redis_cache do
+RSpec.describe Gitlab::GithubImport::LabelFinder, :clean_gitlab_redis_cache, feature_category: :importers do
let_it_be(:project) { create(:project) }
let_it_be(:finder) { described_class.new(project) }
let_it_be(:bug) { create(:label, project: project, name: 'Bug') }
@@ -18,23 +18,64 @@ RSpec.describe Gitlab::GithubImport::LabelFinder, :clean_gitlab_redis_cache do
expect(finder.id_for(feature.name)).to eq(feature.id)
end
- it 'returns nil for an empty cache key' do
+ it 'fetches object id from database if not in cache' do
key = finder.cache_key_for(bug.name)
Gitlab::Cache::Import::Caching.write(key, '')
- expect(finder.id_for(bug.name)).to be_nil
+ expect(finder.id_for(bug.name)).to eq(bug.id)
end
it 'returns nil for a non existing label name' do
expect(finder.id_for('kittens')).to be_nil
end
+
+ it 'returns nil and skips database read if cache has no record' do
+ key = finder.cache_key_for(bug.name)
+
+ Gitlab::Cache::Import::Caching.write(key, -1)
+
+ expect(finder.id_for(bug.name)).to be_nil
+ end
end
context 'without a cache in place' do
- it 'returns nil for a label' do
+ it 'caches the ID of a database row and returns the ID' do
+ expect(Gitlab::Cache::Import::Caching)
+ .to receive(:write)
+ .with("github-import/label-finder/#{project.id}/#{feature.name}", feature.id)
+ .and_call_original
+
+ expect(finder.id_for(feature.name)).to eq(feature.id)
+ end
+ end
+
+ context 'with FF import_fallback_to_db_empty_cache disabled' do
+ before do
+ stub_feature_flags(import_fallback_to_db_empty_cache: false)
+ end
+
+ it 'returns nil for a non existing label name' do
+ expect(finder.id_for('kittens')).to be_nil
+ end
+
+ it 'does not fetch object id from database if not in cache' do
expect(finder.id_for(feature.name)).to be_nil
end
+
+ it 'fetches object id from cache if present' do
+ finder.build_cache
+
+ expect(finder.id_for(feature.name)).to eq(feature.id)
+ end
+
+ it 'returns -1 if cache is -1' do
+ key = finder.cache_key_for(bug.name)
+
+ Gitlab::Cache::Import::Caching.write(key, -1)
+
+ expect(finder.id_for(bug.name)).to eq(-1)
+ end
end
end
diff --git a/spec/lib/gitlab/github_import/milestone_finder_spec.rb b/spec/lib/gitlab/github_import/milestone_finder_spec.rb
index e7f47d334e8..62886981de1 100644
--- a/spec/lib/gitlab/github_import/milestone_finder_spec.rb
+++ b/spec/lib/gitlab/github_import/milestone_finder_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::GithubImport::MilestoneFinder, :clean_gitlab_redis_cache do
+RSpec.describe Gitlab::GithubImport::MilestoneFinder, :clean_gitlab_redis_cache, feature_category: :importers do
let_it_be(:project) { create(:project) }
let_it_be(:milestone) { create(:milestone, project: project) }
@@ -20,23 +20,72 @@ RSpec.describe Gitlab::GithubImport::MilestoneFinder, :clean_gitlab_redis_cache
expect(finder.id_for(issuable)).to eq(milestone.id)
end
- it 'returns nil for an empty cache key' do
+ it 'returns nil if object does not exist' do
+ missing_issuable = double(:issuable, milestone_number: 999)
+
+ expect(finder.id_for(missing_issuable)).to be_nil
+ end
+
+ it 'fetches object id from database if not in cache' do
key = finder.cache_key_for(milestone.iid)
Gitlab::Cache::Import::Caching.write(key, '')
- expect(finder.id_for(issuable)).to be_nil
+ expect(finder.id_for(issuable)).to eq(milestone.id)
end
it 'returns nil for an issuable with a non-existing milestone' do
expect(finder.id_for(double(:issuable, milestone_number: 5))).to be_nil
end
+
+ it 'returns nil and skips database read if cache has no record' do
+ key = finder.cache_key_for(milestone.iid)
+
+ Gitlab::Cache::Import::Caching.write(key, -1)
+
+ expect(finder.id_for(issuable)).to be_nil
+ end
end
context 'without a cache in place' do
- it 'returns nil' do
+ it 'caches the ID of a database row and returns the ID' do
+ expect(Gitlab::Cache::Import::Caching)
+ .to receive(:write)
+ .with("github-import/milestone-finder/#{project.id}/1", milestone.id)
+ .and_call_original
+
+ expect(finder.id_for(issuable)).to eq(milestone.id)
+ end
+ end
+
+ context 'with FF import_fallback_to_db_empty_cache disabled' do
+ before do
+ stub_feature_flags(import_fallback_to_db_empty_cache: false)
+ end
+
+ it 'returns nil if object does not exist' do
+ missing_issuable = double(:issuable, milestone_number: 999)
+
+ expect(finder.id_for(missing_issuable)).to be_nil
+ end
+
+ it 'does not fetch object id from database if not in cache' do
expect(finder.id_for(issuable)).to be_nil
end
+
+ it 'fetches object id from cache if present' do
+ finder.build_cache
+
+ expect(finder.id_for(issuable)).to eq(milestone.id)
+ end
+
+ it 'returns -1 if cache is -1' do
+ key = finder.cache_key_for(milestone.iid)
+
+ Gitlab::Cache::Import::Caching.write(key, -1)
+
+ expect(finder.id_for(issuable)).to eq(-1)
+ end
end
end
diff --git a/spec/lib/gitlab/github_import/object_counter_spec.rb b/spec/lib/gitlab/github_import/object_counter_spec.rb
index e41a2cff989..964bdd6aad1 100644
--- a/spec/lib/gitlab/github_import/object_counter_spec.rb
+++ b/spec/lib/gitlab/github_import/object_counter_spec.rb
@@ -68,6 +68,16 @@ RSpec.describe Gitlab::GithubImport::ObjectCounter, :clean_gitlab_redis_cache, f
'imported' => { 'issue' => 8 }
)
end
+
+ it 'uses the same TTL as when incrementing' do
+ expect(Gitlab::Cache::Import::Caching)
+ .to receive(:read_integer)
+ .with(anything, timeout: described_class::IMPORT_CACHING_TIMEOUT)
+ .twice
+ .and_call_original
+
+ described_class.summary(project)
+ end
end
context 'when import is in progress but cache expired' do
diff --git a/spec/lib/gitlab/github_import/parallel_scheduling_spec.rb b/spec/lib/gitlab/github_import/parallel_scheduling_spec.rb
index 9de39a3ff7e..e0b1ff1bc33 100644
--- a/spec/lib/gitlab/github_import/parallel_scheduling_spec.rb
+++ b/spec/lib/gitlab/github_import/parallel_scheduling_spec.rb
@@ -296,11 +296,11 @@ RSpec.describe Gitlab::GithubImport::ParallelScheduling, feature_category: :impo
expect(importer).to receive(:each_object_to_import)
.and_yield(object).and_yield(object).and_yield(object)
expect(worker_class).to receive(:perform_in)
- .with(1.second, project.id, { title: 'One' }, 'waiter-key').ordered
+ .with(1, project.id, { 'title' => 'One' }, 'waiter-key').ordered
expect(worker_class).to receive(:perform_in)
- .with(1.second, project.id, { title: 'Two' }, 'waiter-key').ordered
+ .with(1, project.id, { 'title' => 'Two' }, 'waiter-key').ordered
expect(worker_class).to receive(:perform_in)
- .with(1.minute + 1.second, project.id, { title: 'Three' }, 'waiter-key').ordered
+ .with(61, project.id, { 'title' => 'Three' }, 'waiter-key').ordered
job_waiter = importer.parallel_import
@@ -325,11 +325,11 @@ RSpec.describe Gitlab::GithubImport::ParallelScheduling, feature_category: :impo
expect(importer).to receive(:each_object_to_import).and_yield(object).and_yield(object).and_yield(object)
expect(worker_class).to receive(:perform_in)
- .with(1.second, project.id, { title: 'One' }, 'waiter-key').ordered
+ .with(1, project.id, { 'title' => 'One' }, 'waiter-key').ordered
expect(worker_class).to receive(:perform_in)
- .with(1.minute + 1.second, project.id, { title: 'Two' }, 'waiter-key').ordered
+ .with(61, project.id, { 'title' => 'Two' }, 'waiter-key').ordered
expect(worker_class).to receive(:perform_in)
- .with(2.minutes + 1.second, project.id, { title: 'Three' }, 'waiter-key').ordered
+ .with(121, project.id, { 'title' => 'Three' }, 'waiter-key').ordered
job_waiter = importer.parallel_import
diff --git a/spec/lib/gitlab/github_import/representation/to_hash_spec.rb b/spec/lib/gitlab/github_import/representation/to_hash_spec.rb
index 739c832025c..52edffe586d 100644
--- a/spec/lib/gitlab/github_import/representation/to_hash_spec.rb
+++ b/spec/lib/gitlab/github_import/representation/to_hash_spec.rb
@@ -2,14 +2,14 @@
require 'fast_spec_helper'
-RSpec.describe Gitlab::GithubImport::Representation::ToHash do
+RSpec.describe Gitlab::GithubImport::Representation::ToHash, feature_category: :importers do
describe '#to_hash' do
let(:user) { double(:user, attributes: { login: 'alice' }) }
let(:issue) do
double(
:issue,
- attributes: { user: user, assignees: [user], number: 42 }
+ attributes: { user: user, assignees: [user], number: 42, created_at: 5.days.ago, status: :valid }
)
end
@@ -35,5 +35,13 @@ RSpec.describe Gitlab::GithubImport::Representation::ToHash do
it 'keeps values as-is if they do not respond to #to_hash' do
expect(issue_hash[:number]).to eq(42)
end
+
+ it 'converts Date value to String' do
+ expect(issue_hash[:created_at]).to be_an_instance_of(String)
+ end
+
+ it 'converts Symbol value to String' do
+ expect(issue_hash[:status]).to be_an_instance_of(String)
+ end
end
end
diff --git a/spec/lib/gitlab/graphql/known_operations_spec.rb b/spec/lib/gitlab/graphql/known_operations_spec.rb
index c7bc47e1e6a..acb85bc737b 100644
--- a/spec/lib/gitlab/graphql/known_operations_spec.rb
+++ b/spec/lib/gitlab/graphql/known_operations_spec.rb
@@ -7,7 +7,7 @@ RSpec.describe Gitlab::Graphql::KnownOperations do
using RSpec::Parameterized::TableSyntax
# Include duplicated operation names to test that we are unique-ifying them
- let(:fake_operations) { %w(foo foo bar bar) }
+ let(:fake_operations) { %w[foo foo bar bar] }
let(:fake_schema) do
Class.new(GraphQL::Schema) do
query Graphql::FakeQueryType
@@ -34,7 +34,7 @@ RSpec.describe Gitlab::Graphql::KnownOperations do
describe "#operations" do
it "returns array of known operations" do
- expect(subject.operations.map(&:name)).to match_array(%w(unknown foo bar))
+ expect(subject.operations.map(&:name)).to match_array(%w[unknown foo bar])
end
end
diff --git a/spec/lib/gitlab/graphql/tracers/metrics_tracer_spec.rb b/spec/lib/gitlab/graphql/tracers/metrics_tracer_spec.rb
index f0312293469..f077cff6875 100644
--- a/spec/lib/gitlab/graphql/tracers/metrics_tracer_spec.rb
+++ b/spec/lib/gitlab/graphql/tracers/metrics_tracer_spec.rb
@@ -6,7 +6,7 @@ require 'rspec-parameterized'
RSpec.describe Gitlab::Graphql::Tracers::MetricsTracer do
using RSpec::Parameterized::TableSyntax
- let(:default_known_operations) { ::Gitlab::Graphql::KnownOperations.new(%w(lorem foo bar)) }
+ let(:default_known_operations) { ::Gitlab::Graphql::KnownOperations.new(%w[lorem foo bar]) }
let(:fake_schema) do
Class.new(GraphQL::Schema) do
diff --git a/spec/lib/gitlab/group_search_results_spec.rb b/spec/lib/gitlab/group_search_results_spec.rb
index 84a2a0549d5..8466e8a1bb5 100644
--- a/spec/lib/gitlab/group_search_results_spec.rb
+++ b/spec/lib/gitlab/group_search_results_spec.rb
@@ -48,7 +48,7 @@ RSpec.describe Gitlab::GroupSearchResults, feature_category: :global_search do
end
include_examples 'search results filtered by state'
- include_examples 'search results filtered by archived', 'search_merge_requests_hide_archived_projects'
+ include_examples 'search results filtered by archived'
end
describe 'milestones search' do
diff --git a/spec/lib/gitlab/hashed_path_spec.rb b/spec/lib/gitlab/hashed_path_spec.rb
index 051c5196748..cf31e891957 100644
--- a/spec/lib/gitlab/hashed_path_spec.rb
+++ b/spec/lib/gitlab/hashed_path_spec.rb
@@ -18,7 +18,7 @@ RSpec.describe Gitlab::HashedPath do
end
context 'when path contains multiple values' do
- let(:path) { %w(path1 path2) }
+ let(:path) { %w[path1 path2] }
it 'returns the disk path' do
expect(subject).to match(%r[\h{2}/\h{2}/\h{64}/path1/path2])
diff --git a/spec/lib/gitlab/health_checks/gitaly_check_spec.rb b/spec/lib/gitlab/health_checks/gitaly_check_spec.rb
index 64c4e92f80b..8f676d20c22 100644
--- a/spec/lib/gitlab/health_checks/gitaly_check_spec.rb
+++ b/spec/lib/gitlab/health_checks/gitaly_check_spec.rb
@@ -4,11 +4,6 @@ require 'fast_spec_helper'
RSpec.describe Gitlab::HealthChecks::GitalyCheck do
let(:result_class) { Gitlab::HealthChecks::Result }
- let(:repository_storages) { ['default'] }
-
- before do
- allow(described_class).to receive(:repository_storages) { repository_storages }
- end
describe '#readiness' do
subject { described_class.readiness }
diff --git a/spec/lib/gitlab/highlight_spec.rb b/spec/lib/gitlab/highlight_spec.rb
index 173131b1d5c..ef3765e479f 100644
--- a/spec/lib/gitlab/highlight_spec.rb
+++ b/spec/lib/gitlab/highlight_spec.rb
@@ -43,7 +43,7 @@ RSpec.describe Gitlab::Highlight do
it 'returns plain version for unknown lexer context' do
result = described_class.highlight(plain_text_file_name, plain_text_content)
- expect(result).to eq(%[<span id="LC1" class="line" lang="plaintext">plain text contents</span>])
+ expect(result).to eq(%(<span id="LC1" class="line" lang="plaintext">plain text contents</span>))
end
context 'when content is too long to be highlighted' do
diff --git a/spec/lib/gitlab/i18n/translation_entry_spec.rb b/spec/lib/gitlab/i18n/translation_entry_spec.rb
index df503e68cf1..3531314cf9c 100644
--- a/spec/lib/gitlab/i18n/translation_entry_spec.rb
+++ b/spec/lib/gitlab/i18n/translation_entry_spec.rb
@@ -108,7 +108,7 @@ RSpec.describe Gitlab::I18n::TranslationEntry do
describe '#msgid_contains_newlines' do
it 'is true when the msgid is an array' do
- data = { msgid: %w(hello world) }
+ data = { msgid: %w[hello world] }
entry = described_class.new(entry_data: data, nplurals: 2)
expect(entry.msgid_has_multiple_lines?).to be_truthy
@@ -117,7 +117,7 @@ RSpec.describe Gitlab::I18n::TranslationEntry do
describe '#plural_id_contains_newlines' do
it 'is true when the msgid is an array' do
- data = { msgid_plural: %w(hello world) }
+ data = { msgid_plural: %w[hello world] }
entry = described_class.new(entry_data: data, nplurals: 2)
expect(entry.plural_id_has_multiple_lines?).to be_truthy
@@ -126,7 +126,7 @@ RSpec.describe Gitlab::I18n::TranslationEntry do
describe '#translations_contain_newlines' do
it 'is true when the msgid is an array' do
- data = { msgstr: %w(hello world) }
+ data = { msgstr: %w[hello world] }
entry = described_class.new(entry_data: data, nplurals: 2)
expect(entry.translations_have_multiple_lines?).to be_truthy
diff --git a/spec/lib/gitlab/import/import_failure_service_spec.rb b/spec/lib/gitlab/import/import_failure_service_spec.rb
index a4682a9495e..362d809bb56 100644
--- a/spec/lib/gitlab/import/import_failure_service_spec.rb
+++ b/spec/lib/gitlab/import/import_failure_service_spec.rb
@@ -10,7 +10,7 @@ RSpec.describe Gitlab::Import::ImportFailureService, :aggregate_failures, featur
let(:import_state) { nil }
let(:fail_import) { false }
let(:metrics) { false }
- let(:external_identifiers) { {} }
+ let(:external_identifiers) { { foo: 'bar' } }
let(:project_id) { project.id }
let(:arguments) do
@@ -90,13 +90,19 @@ RSpec.describe Gitlab::Import::ImportFailureService, :aggregate_failures, featur
)
service.execute
-
- expect(project.import_state.reload.status).to eq('failed')
-
- expect(project.import_failures).not_to be_empty
- expect(project.import_failures.last.exception_class).to eq('StandardError')
- expect(project.import_failures.last.exception_message).to eq('some error')
- expect(project.import_failures.last.retry_count).to eq(0)
+ project.reload
+
+ expect(project.import_state.status).to eq('failed')
+ expect(project.import_failures).to contain_exactly(
+ have_attributes(
+ retry_count: 0,
+ exception_class: 'StandardError',
+ exception_message: 'some error',
+ external_identifiers: external_identifiers.with_indifferent_access,
+ correlation_id_value: Labkit::Correlation::CorrelationId.current_or_new_id,
+ source: 'SomeImporter'
+ )
+ )
end
end
@@ -128,13 +134,19 @@ RSpec.describe Gitlab::Import::ImportFailureService, :aggregate_failures, featur
)
service.execute
+ project.reload
expect(project.import_state.reload.status).to eq('started')
-
- expect(project.import_failures).not_to be_empty
- expect(project.import_failures.last.exception_class).to eq('StandardError')
- expect(project.import_failures.last.exception_message).to eq('some error')
- expect(project.import_failures.last.retry_count).to eq(nil)
+ expect(project.import_failures).to contain_exactly(
+ have_attributes(
+ retry_count: nil,
+ exception_class: 'StandardError',
+ exception_message: 'some error',
+ external_identifiers: external_identifiers.with_indifferent_access,
+ correlation_id_value: Labkit::Correlation::CorrelationId.current_or_new_id,
+ source: 'SomeImporter'
+ )
+ )
end
end
diff --git a/spec/lib/gitlab/import_export/after_export_strategies/base_after_export_strategy_spec.rb b/spec/lib/gitlab/import_export/after_export_strategies/base_after_export_strategy_spec.rb
index fc794f11499..2046e1b5ae5 100644
--- a/spec/lib/gitlab/import_export/after_export_strategies/base_after_export_strategy_spec.rb
+++ b/spec/lib/gitlab/import_export/after_export_strategies/base_after_export_strategy_spec.rb
@@ -100,7 +100,7 @@ RSpec.describe Gitlab::ImportExport::AfterExportStrategies::BaseAfterExportStrat
describe '#log_validation_errors' do
it 'add the message to the shared context' do
- errors = %w(test_message test_message2)
+ errors = %w[test_message test_message2]
allow(service).to receive(:invalid?).and_return(true)
allow(service.errors).to receive(:full_messages).and_return(errors)
diff --git a/spec/lib/gitlab/import_export/after_export_strategies/web_upload_strategy_spec.rb b/spec/lib/gitlab/import_export/after_export_strategies/web_upload_strategy_spec.rb
index 297fe3ade07..0f9cbe8aea3 100644
--- a/spec/lib/gitlab/import_export/after_export_strategies/web_upload_strategy_spec.rb
+++ b/spec/lib/gitlab/import_export/after_export_strategies/web_upload_strategy_spec.rb
@@ -27,7 +27,7 @@ RSpec.describe Gitlab::ImportExport::AfterExportStrategies::WebUploadStrategy do
describe 'validations' do
it 'only POST and PUT method allowed' do
- %w(POST post PUT put).each do |method|
+ %w[POST post PUT put].each do |method|
expect(subject.new(url: example_url, http_method: method)).to be_valid
end
diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml
index cd899a79451..722b47ac9b8 100644
--- a/spec/lib/gitlab/import_export/all_models.yml
+++ b/spec/lib/gitlab/import_export/all_models.yml
@@ -660,6 +660,7 @@ project:
- pages_domains
- pages_metadatum
- pages_deployments
+- active_pages_deployments
- authorized_users
- project_authorizations
- remote_mirrors
@@ -1061,6 +1062,7 @@ approval_rules:
- users
- groups
- group_users
+ - group_members
- security_orchestration_policy_configuration
- protected_branches
- approval_merge_request_rule_sources
diff --git a/spec/lib/gitlab/import_export/attribute_cleaner_spec.rb b/spec/lib/gitlab/import_export/attribute_cleaner_spec.rb
index 272c2629b08..9d69e1fec05 100644
--- a/spec/lib/gitlab/import_export/attribute_cleaner_spec.rb
+++ b/spec/lib/gitlab/import_export/attribute_cleaner_spec.rb
@@ -34,7 +34,7 @@ RSpec.describe Gitlab::ImportExport::AttributeCleaner do
'note_ids' => [1, 2, 3],
'remote_attachment_url' => 'http://something.dodgy',
'remote_attachment_request_header' => 'bad value',
- 'remote_attachment_urls' => %w(http://something.dodgy http://something.okay),
+ 'remote_attachment_urls' => %w[http://something.dodgy http://something.okay],
'attributes' => {
'issue_ids' => [1, 2, 3],
'merge_request_ids' => [1, 2, 3],
diff --git a/spec/lib/gitlab/import_export/attributes_permitter_spec.rb b/spec/lib/gitlab/import_export/attributes_permitter_spec.rb
index 08abd7908d2..996b32ed341 100644
--- a/spec/lib/gitlab/import_export/attributes_permitter_spec.rb
+++ b/spec/lib/gitlab/import_export/attributes_permitter_spec.rb
@@ -33,7 +33,7 @@ RSpec.describe Gitlab::ImportExport::AttributesPermitter, feature_category: :imp
EOF
end
- let(:file) { Tempfile.new(%w(import_export .yml)) }
+ let(:file) { Tempfile.new(%w[import_export .yml]) }
let(:config_hash) { Gitlab::ImportExport::Config.new(config: file.path).to_h }
before do
diff --git a/spec/lib/gitlab/import_export/command_line_util_spec.rb b/spec/lib/gitlab/import_export/command_line_util_spec.rb
index 42c3b170e4d..ab47de8f874 100644
--- a/spec/lib/gitlab/import_export/command_line_util_spec.rb
+++ b/spec/lib/gitlab/import_export/command_line_util_spec.rb
@@ -263,7 +263,11 @@ RSpec.describe Gitlab::ImportExport::CommandLineUtil, feature_category: :importe
context 'when exception occurs' do
it 'raises an exception' do
- expect { subject.gzip(dir: path, filename: 'test') }.to raise_error(Gitlab::ImportExport::Error)
+ expect { subject.gzip(dir: path, filename: 'test') }
+ .to raise_error(
+ Gitlab::ImportExport::Error,
+ %r{File compression or decompression failed. Command exited with error code 1: gzip}
+ )
end
end
end
@@ -283,7 +287,11 @@ RSpec.describe Gitlab::ImportExport::CommandLineUtil, feature_category: :importe
context 'when exception occurs' do
it 'raises an exception' do
- expect { subject.gunzip(dir: path, filename: 'test') }.to raise_error(Gitlab::ImportExport::Error)
+ expect { subject.gunzip(dir: path, filename: 'test') }
+ .to raise_error(
+ Gitlab::ImportExport::Error,
+ %r{File compression or decompression failed. Command exited with error code 1: gzip}
+ )
end
end
end
@@ -306,7 +314,7 @@ RSpec.describe Gitlab::ImportExport::CommandLineUtil, feature_category: :importe
include Gitlab::ImportExport::CommandLineUtil
end.new
- expect { klass.tar_cf(archive: 'test', dir: 'test') }.to raise_error(Gitlab::ImportExport::Error, 'command exited with error code 1: Error')
+ expect { klass.tar_cf(archive: 'test', dir: 'test') }.to raise_error(Gitlab::ImportExport::Error, 'Command exited with error code 1: Error')
end
end
end
@@ -363,7 +371,7 @@ RSpec.describe Gitlab::ImportExport::CommandLineUtil, feature_category: :importe
include Gitlab::ImportExport::CommandLineUtil
end.new
- expect { klass.untar_xf(archive: 'test', dir: 'test') }.to raise_error(Gitlab::ImportExport::Error, 'command exited with error code 1: Error')
+ expect { klass.untar_xf(archive: 'test', dir: 'test') }.to raise_error(Gitlab::ImportExport::Error, 'Command exited with error code 1: Error')
end
it 'returns false and includes error status' do
@@ -378,7 +386,7 @@ RSpec.describe Gitlab::ImportExport::CommandLineUtil, feature_category: :importe
end.new
expect(klass.tar_czf(archive: 'test', dir: 'test')).to eq(false)
- expect(klass.shared.errors).to eq(['command exited with error code 1: Error'])
+ expect(klass.shared.errors).to eq(['Command exited with error code 1: Error'])
end
end
end
diff --git a/spec/lib/gitlab/import_export/error_spec.rb b/spec/lib/gitlab/import_export/error_spec.rb
index 015133a399b..db16d0f1e45 100644
--- a/spec/lib/gitlab/import_export/error_spec.rb
+++ b/spec/lib/gitlab/import_export/error_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::ImportExport::Error do
+RSpec.describe Gitlab::ImportExport::Error, feature_category: :importers do
describe '.permission_error' do
subject(:error) do
described_class.permission_error(user, importable)
@@ -28,4 +28,12 @@ RSpec.describe Gitlab::ImportExport::Error do
end
end
end
+
+ describe '.file_compression_error' do
+ it 'adds error to exception message' do
+ message = described_class.file_compression_error('Error').message
+
+ expect(message).to eq('File compression or decompression failed. Error')
+ end
+ end
end
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 dfc7202194d..f6ad3e47c30 100644
--- a/spec/lib/gitlab/import_export/fast_hash_serializer_spec.rb
+++ b/spec/lib/gitlab/import_export/fast_hash_serializer_spec.rb
@@ -156,7 +156,7 @@ RSpec.describe Gitlab::ImportExport::FastHashSerializer, :with_license, feature_
it 'has project and group labels' do
label_types = subject['issues'].first['label_links'].map { |link| link['label']['type'] }
- expect(label_types).to match_array(%w(ProjectLabel GroupLabel))
+ expect(label_types).to match_array(%w[ProjectLabel GroupLabel])
end
it 'has priorities associated to labels' do
diff --git a/spec/lib/gitlab/import_export/json/ndjson_writer_spec.rb b/spec/lib/gitlab/import_export/json/ndjson_writer_spec.rb
index 486d179ae05..a7aeb9e8c3b 100644
--- a/spec/lib/gitlab/import_export/json/ndjson_writer_spec.rb
+++ b/spec/lib/gitlab/import_export/json/ndjson_writer_spec.rb
@@ -54,7 +54,7 @@ RSpec.describe Gitlab::ImportExport::Json::NdjsonWriter, feature_category: :impo
describe "#write_relation_array" do
it "writes json in correct files" do
values = [{ "key" => "value_1", "key_1" => "value_1" }, { "key" => "value_2", "key_1" => "value_2" }]
- relations = %w(relation1 relation2)
+ relations = %w[relation1 relation2]
relations.each do |relation|
subject.write_relation_array(exportable_path, relation, values.to_enum)
end
diff --git a/spec/lib/gitlab/import_export/lfs_restorer_spec.rb b/spec/lib/gitlab/import_export/lfs_restorer_spec.rb
index fe064c50b9e..042a49f9419 100644
--- a/spec/lib/gitlab/import_export/lfs_restorer_spec.rb
+++ b/spec/lib/gitlab/import_export/lfs_restorer_spec.rb
@@ -27,7 +27,7 @@ RSpec.describe Gitlab::ImportExport::LfsRestorer do
# Use the LfsSaver to save data to be restored
def save_lfs_data
- %w(project wiki).each do |repository_type|
+ %w[project wiki].each do |repository_type|
create(
:lfs_objects_project,
project: project,
diff --git a/spec/lib/gitlab/import_export/lfs_saver_spec.rb b/spec/lib/gitlab/import_export/lfs_saver_spec.rb
index 5b6f50025ff..bd225265ef0 100644
--- a/spec/lib/gitlab/import_export/lfs_saver_spec.rb
+++ b/spec/lib/gitlab/import_export/lfs_saver_spec.rb
@@ -60,7 +60,7 @@ RSpec.describe Gitlab::ImportExport::LfsSaver do
describe 'saving a json file' do
before do
# Create two more LfsObjectProject records with different `repository_type`s
- %w(wiki design).each do |repository_type|
+ %w[wiki design].each do |repository_type|
create(
:lfs_objects_project,
project: project,
diff --git a/spec/lib/gitlab/import_export/project/tree_saver_spec.rb b/spec/lib/gitlab/import_export/project/tree_saver_spec.rb
index 1bf1e5b47e1..2e82351db10 100644
--- a/spec/lib/gitlab/import_export/project/tree_saver_spec.rb
+++ b/spec/lib/gitlab/import_export/project/tree_saver_spec.rb
@@ -190,7 +190,7 @@ RSpec.describe Gitlab::ImportExport::Project::TreeSaver, :with_license, feature_
it 'has project and group labels' do
label_types = subject.first['label_links'].map { |link| link['label']['type'] }
- expect(label_types).to match_array(%w(ProjectLabel GroupLabel))
+ expect(label_types).to match_array(%w[ProjectLabel GroupLabel])
end
it 'has priorities associated to labels' do
diff --git a/spec/lib/gitlab/import_export/saver_spec.rb b/spec/lib/gitlab/import_export/saver_spec.rb
index a34e68ecd19..ba38a5d7960 100644
--- a/spec/lib/gitlab/import_export/saver_spec.rb
+++ b/spec/lib/gitlab/import_export/saver_spec.rb
@@ -33,7 +33,7 @@ RSpec.describe Gitlab::ImportExport::Saver do
subject.save # rubocop:disable Rails/SaveBang
expect(ImportExportUpload.find_by(project: project).export_file.url)
- .to match(%r[/uploads/-/system/import_export_upload/export_file.*])
+ .to match(%r{/uploads/-/system/import_export_upload/export_file.*})
end
it 'logs metrics after saving' do
diff --git a/spec/lib/gitlab/import_export/snippet_repo_restorer_spec.rb b/spec/lib/gitlab/import_export/snippet_repo_restorer_spec.rb
index d7b1b180e2e..97e3caba9b3 100644
--- a/spec/lib/gitlab/import_export/snippet_repo_restorer_spec.rb
+++ b/spec/lib/gitlab/import_export/snippet_repo_restorer_spec.rb
@@ -31,7 +31,7 @@ RSpec.describe Gitlab::ImportExport::SnippetRepoRestorer do
expect(restorer.restore).to be_truthy
end.to change { SnippetRepository.count }.by(1)
- snippet.repository.expire_method_caches(%i(exists?))
+ snippet.repository.expire_method_caches(%i[exists?])
expect(snippet.repository_exists?).to be_truthy
blob = snippet.repository.blob_at(snippet.default_branch, snippet.file_name)
diff --git a/spec/lib/gitlab/import_sources_spec.rb b/spec/lib/gitlab/import_sources_spec.rb
index db23e3b1fd4..19f17c9079d 100644
--- a/spec/lib/gitlab/import_sources_spec.rb
+++ b/spec/lib/gitlab/import_sources_spec.rb
@@ -24,7 +24,7 @@ RSpec.describe Gitlab::ImportSources, feature_category: :importers do
describe '.values' do
it 'returns an array' do
expected =
- %w(
+ %w[
github
bitbucket
bitbucket_server
@@ -33,7 +33,7 @@ RSpec.describe Gitlab::ImportSources, feature_category: :importers do
gitlab_project
gitea
manifest
- )
+ ]
expect(described_class.values).to eq(expected)
end
@@ -42,14 +42,14 @@ RSpec.describe Gitlab::ImportSources, feature_category: :importers do
describe '.importer_names' do
it 'returns an array of importer names' do
expected =
- %w(
+ %w[
github
bitbucket
bitbucket_server
fogbugz
gitlab_project
gitea
- )
+ ]
expect(described_class.importer_names).to eq(expected)
end
diff --git a/spec/lib/gitlab/instrumentation/redis_cluster_validator_spec.rb b/spec/lib/gitlab/instrumentation/redis_cluster_validator_spec.rb
index ddb5245f825..ea5a32a25ff 100644
--- a/spec/lib/gitlab/instrumentation/redis_cluster_validator_spec.rb
+++ b/spec/lib/gitlab/instrumentation/redis_cluster_validator_spec.rb
@@ -11,25 +11,25 @@ RSpec.describe Gitlab::Instrumentation::RedisClusterValidator, feature_category:
using RSpec::Parameterized::TableSyntax
where(:command, :arguments, :keys, :is_valid) do
- :rename | %w(foo bar) | 2 | false
- :RENAME | %w(foo bar) | 2 | false
- 'rename' | %w(foo bar) | 2 | false
- 'RENAME' | %w(foo bar) | 2 | false
- :rename | %w(iaa ahy) | 2 | true # 'iaa' and 'ahy' hash to the same slot
- :rename | %w({foo}:1 {foo}:2) | 2 | true
- :rename | %w(foo foo bar) | 2 | true # This is not a valid command but should not raise here
- :mget | %w(foo bar) | 2 | false
- :mget | %w(foo foo bar) | 3 | false
- :mget | %w(foo foo) | 2 | true
- :blpop | %w(foo bar 1) | 2 | false
- :blpop | %w(foo foo 1) | 2 | true
- :mset | %w(foo a bar a) | 2 | false
- :mset | %w(foo a foo a) | 2 | true
- :del | %w(foo bar) | 2 | false
- :del | [%w(foo bar)] | 2 | false # Arguments can be a nested array
- :del | %w(foo foo) | 2 | true
- :hset | %w(foo bar) | 1 | nil # Single key write
- :get | %w(foo) | 1 | nil # Single key read
+ :rename | %w[foo bar] | 2 | false
+ :RENAME | %w[foo bar] | 2 | false
+ 'rename' | %w[foo bar] | 2 | false
+ 'RENAME' | %w[foo bar] | 2 | false
+ :rename | %w[iaa ahy] | 2 | true # 'iaa' and 'ahy' hash to the same slot
+ :rename | %w[{foo}:1 {foo}:2] | 2 | true
+ :rename | %w[foo foo bar] | 2 | true # This is not a valid command but should not raise here
+ :mget | %w[foo bar] | 2 | false
+ :mget | %w[foo foo bar] | 3 | false
+ :mget | %w[foo foo] | 2 | true
+ :blpop | %w[foo bar 1] | 2 | false
+ :blpop | %w[foo foo 1] | 2 | true
+ :mset | %w[foo a bar a] | 2 | false
+ :mset | %w[foo a foo a] | 2 | true
+ :del | %w[foo bar] | 2 | false
+ :del | [%w[foo bar]] | 2 | false # Arguments can be a nested array
+ :del | %w[foo foo] | 2 | true
+ :hset | %w[foo bar] | 1 | nil # Single key write
+ :get | %w[foo] | 1 | nil # Single key read
:mget | [] | 0 | true # This is invalid, but not because it's a cross-slot command
end
diff --git a/spec/lib/gitlab/instrumentation_helper_spec.rb b/spec/lib/gitlab/instrumentation_helper_spec.rb
index 698c8a37d48..f8a4d8023c1 100644
--- a/spec/lib/gitlab/instrumentation_helper_spec.rb
+++ b/spec/lib/gitlab/instrumentation_helper_spec.rb
@@ -7,6 +7,7 @@ require 'support/helpers/rails_helpers'
RSpec.describe Gitlab::InstrumentationHelper, :clean_gitlab_redis_repository_cache, :clean_gitlab_redis_cache,
:use_null_store_as_repository_cache, feature_category: :scalability do
using RSpec::Parameterized::TableSyntax
+ include RedisHelpers
describe '.add_instrumentation_data', :request_store do
let(:payload) { {} }
@@ -39,11 +40,23 @@ RSpec.describe Gitlab::InstrumentationHelper, :clean_gitlab_redis_repository_cac
end
context 'when Redis calls are made' do
- it 'adds Redis data and omits Gitaly data' do
- stub_rails_env('staging') # to avoid raising CrossSlotError
- Gitlab::Redis::Sessions.with { |redis| redis.mset('test-cache', 123, 'test-cache2', 123) }
+ let_it_be(:redis_store_class) { define_helper_redis_store_class }
+
+ before do
+ redis_store_class.with(&:ping)
+ Gitlab::Redis::Queues.with(&:ping)
+ RequestStore.clear!
+ end
+
+ it 'adds Redis data including cross slot calls' do
+ expect(Gitlab::Instrumentation::RedisBase)
+ .to receive(:raise_cross_slot_validation_errors?)
+ .once.and_return(false)
+
+ redis_store_class.with { |redis| redis.mset('test-cache', 123, 'test-cache2', 123) }
+
Gitlab::Instrumentation::RedisClusterValidator.allow_cross_slot_commands do
- Gitlab::Redis::Sessions.with { |redis| redis.mget('cache-test', 'cache-test-2') }
+ redis_store_class.with { |redis| redis.mget('cache-test', 'cache-test-2') }
end
Gitlab::Redis::Queues.with { |redis| redis.set('test-queues', 321) }
@@ -249,15 +262,12 @@ RSpec.describe Gitlab::InstrumentationHelper, :clean_gitlab_redis_repository_cac
end
end
- describe 'duration calculations' do
- where(:end_time, :start_time, :time_now, :expected_duration) do
+ describe '.queue_duration_for_job' do
+ where(:enqueued_at, :created_at, :time_now, :expected_duration) do
"2019-06-01T00:00:00.000+0000" | nil | "2019-06-01T02:00:00.000+0000" | 2.hours.to_f
- "2019-06-01T02:00:00.000+0000" | nil | "2019-06-01T02:00:00.001+0000" | 0.001
"2019-06-01T02:00:00.000+0000" | "2019-05-01T02:00:00.000+0000" | "2019-06-01T02:00:01.000+0000" | 1
- nil | "2019-06-01T02:00:00.000+0000" | "2019-06-01T02:00:00.001+0000" | 0.001
nil | nil | "2019-06-01T02:00:00.001+0000" | nil
"2019-06-01T02:00:00.000+0200" | nil | "2019-06-01T02:00:00.000-0200" | 4.hours.to_f
- 1571825569.998168 | nil | "2019-10-23T12:13:16.000+0200" | 26.001832
1571825569 | nil | "2019-10-23T12:13:16.000+0200" | 27
"invalid_date" | nil | "2019-10-23T12:13:16.000+0200" | nil
"" | nil | "2019-10-23T12:13:16.000+0200" | nil
@@ -267,27 +277,30 @@ RSpec.describe Gitlab::InstrumentationHelper, :clean_gitlab_redis_repository_cac
Time.at(1571999233).utc | nil | "2019-10-25T12:29:16.000+0200" | 123
end
- describe '.queue_duration_for_job' do
- with_them do
- let(:job) { { 'enqueued_at' => end_time, 'created_at' => start_time } }
+ with_them do
+ let(:job) { { 'enqueued_at' => enqueued_at, 'created_at' => created_at } }
- it "returns the correct duration" do
- travel_to(Time.iso8601(time_now)) do
- expect(described_class.queue_duration_for_job(job)).to eq(expected_duration)
- end
+ it "returns the correct duration" do
+ travel_to(Time.iso8601(time_now)) do
+ expect(described_class.queue_duration_for_job(job)).to eq(expected_duration)
end
end
end
+ end
- describe '.enqueue_latency_for_scheduled_job' do
- with_them do
- let(:job) { { 'enqueued_at' => end_time, 'scheduled_at' => start_time } }
+ describe '.enqueue_latency_for_scheduled_job' do
+ where(:scheduled_at, :enqueued_at, :expected_duration) do
+ "2019-06-01T02:00:00.000+0000" | "2019-06-01T02:00:00.001+0000" | 0.001
+ "2019-06-01T02:00:00.000+0000" | "2019-06-01T02:00:01.000+0000" | 1
+ "2019-06-01T02:00:00.000+0000" | nil | nil
+ nil | "2019-06-01T02:00:01.000+0000" | nil
+ end
- it "returns the correct duration" do
- travel_to(Time.iso8601(time_now)) do
- expect(described_class.enqueue_latency_for_scheduled_job(job)).to eq(expected_duration)
- end
- end
+ with_them do
+ let(:job) { { 'enqueued_at' => enqueued_at, 'scheduled_at' => scheduled_at } }
+
+ it "returns the correct duration" do
+ expect(described_class.enqueue_latency_for_scheduled_job(job)).to eq(expected_duration)
end
end
end
diff --git a/spec/lib/gitlab/issues/rebalancing/state_spec.rb b/spec/lib/gitlab/issues/rebalancing/state_spec.rb
index 5adf1328b87..a0ea5fec8ec 100644
--- a/spec/lib/gitlab/issues/rebalancing/state_spec.rb
+++ b/spec/lib/gitlab/issues/rebalancing/state_spec.rb
@@ -67,11 +67,11 @@ RSpec.describe Gitlab::Issues::Rebalancing::State, :clean_gitlab_redis_shared_st
end
it 'returns array of issue ids' do
- expect(rebalance_caching.get_cached_issue_ids(0, 100)).to eq(%w(1 2 3))
+ expect(rebalance_caching.get_cached_issue_ids(0, 100)).to eq(%w[1 2 3])
end
it 'limits returned values' do
- expect(rebalance_caching.get_cached_issue_ids(0, 2)).to eq(%w(1 2))
+ expect(rebalance_caching.get_cached_issue_ids(0, 2)).to eq(%w[1 2])
end
context 'when caching duplicate issue_ids' do
@@ -84,7 +84,7 @@ RSpec.describe Gitlab::Issues::Rebalancing::State, :clean_gitlab_redis_shared_st
end
it 'returns cached issues with latest scores' do
- expect(rebalance_caching.get_cached_issue_ids(0, 100)).to eq(%w(3 2 1))
+ expect(rebalance_caching.get_cached_issue_ids(0, 100)).to eq(%w[3 2 1])
end
end
end
@@ -231,8 +231,16 @@ RSpec.describe Gitlab::Issues::Rebalancing::State, :clean_gitlab_redis_shared_st
def check_existing_keys
index = 0
- # spec only, we do not actually scan keys in the code
- recently_finished_keys_count = Gitlab::Redis::SharedState.with { |redis| redis.scan(0, match: "#{described_class::RECENTLY_FINISHED_REBALANCE_PREFIX}:*") }.last.count
+ cursor = '0'
+ recently_finished_keys_count = 0
+
+ # loop to scan since it may run against a Redis Cluster
+ loop do
+ # spec only, we do not actually scan keys in the code
+ cursor, items = Gitlab::Redis::SharedState.with { |redis| redis.scan(cursor, match: "#{described_class::RECENTLY_FINISHED_REBALANCE_PREFIX}:*") }
+ recently_finished_keys_count += items.count
+ break if cursor == '0'
+ end
index += 1 if rebalance_caching.get_current_index > 0
index += 1 if rebalance_caching.get_current_project_id.present?
diff --git a/spec/lib/gitlab/jira/middleware_spec.rb b/spec/lib/gitlab/jira/middleware_spec.rb
deleted file mode 100644
index 09cf67d0657..00000000000
--- a/spec/lib/gitlab/jira/middleware_spec.rb
+++ /dev/null
@@ -1,40 +0,0 @@
-# frozen_string_literal: true
-
-require 'fast_spec_helper'
-
-RSpec.describe Gitlab::Jira::Middleware do
- let(:app) { double(:app) }
- let(:middleware) { described_class.new(app) }
- let(:jira_user_agent) { 'Jira DVCS Connector Vertigo/5.0.0-D20170810T012915' }
-
- describe '.jira_dvcs_connector?' do
- it 'returns true when DVCS connector' do
- expect(described_class.jira_dvcs_connector?('HTTP_USER_AGENT' => jira_user_agent)).to eq(true)
- end
-
- it 'returns true if user agent starts with "Jira DVCS Connector"' do
- expect(described_class.jira_dvcs_connector?('HTTP_USER_AGENT' => 'Jira DVCS Connector')).to eq(true)
- end
-
- it 'returns false when not DVCS connector' do
- expect(described_class.jira_dvcs_connector?('HTTP_USER_AGENT' => 'pokemon')).to eq(false)
- end
- end
-
- describe '#call' do
- it 'adjusts HTTP_AUTHORIZATION env when request from Jira DVCS user agent' do
- expect(app).to receive(:call).with({ 'HTTP_USER_AGENT' => jira_user_agent,
- 'HTTP_AUTHORIZATION' => 'Bearer hash-123' })
-
- middleware.call('HTTP_USER_AGENT' => jira_user_agent, 'HTTP_AUTHORIZATION' => 'token hash-123')
- end
-
- it 'does not change HTTP_AUTHORIZATION env when request is not from Jira DVCS user agent' do
- env = { 'HTTP_USER_AGENT' => 'Mozilla/5.0', 'HTTP_AUTHORIZATION' => 'token hash-123' }
-
- expect(app).to receive(:call).with(env)
-
- middleware.call(env)
- end
- end
-end
diff --git a/spec/lib/gitlab/jira_import/handle_labels_service_spec.rb b/spec/lib/gitlab/jira_import/handle_labels_service_spec.rb
index b8c0dc64581..82233641778 100644
--- a/spec/lib/gitlab/jira_import/handle_labels_service_spec.rb
+++ b/spec/lib/gitlab/jira_import/handle_labels_service_spec.rb
@@ -11,7 +11,7 @@ RSpec.describe Gitlab::JiraImport::HandleLabelsService do
let_it_be(:other_project_label) { create(:label, title: 'feature') }
let_it_be(:group_label) { create(:group_label, group: group, title: 'dev') }
- let(:jira_labels) { %w(bug feature dev group::new) }
+ let(:jira_labels) { %w[bug feature dev group::new] }
subject { described_class.new(project, jira_labels).execute }
@@ -23,7 +23,7 @@ RSpec.describe Gitlab::JiraImport::HandleLabelsService do
it 'creates the missing labels on the project level' do
expect { subject }.to change { Label.count }.from(3).to(5)
- expect(created_labels.map(&:title)).to match_array(%w(feature group::new))
+ expect(created_labels.map(&:title)).to match_array(%w[feature group::new])
end
it 'returns the id of all labels matching the title' do
@@ -32,7 +32,7 @@ RSpec.describe Gitlab::JiraImport::HandleLabelsService do
end
context 'when no provided jira labels are missing' do
- let(:jira_labels) { %w(bug dev) }
+ let(:jira_labels) { %w[bug dev] }
it 'does not create any new labels' do
expect { subject }.not_to change { Label.count }.from(3)
diff --git a/spec/lib/gitlab/jira_import/issue_serializer_spec.rb b/spec/lib/gitlab/jira_import/issue_serializer_spec.rb
index 30ad24472b4..98958d8a92e 100644
--- a/spec/lib/gitlab/jira_import/issue_serializer_spec.rb
+++ b/spec/lib/gitlab/jira_import/issue_serializer_spec.rb
@@ -28,7 +28,7 @@ RSpec.describe Gitlab::JiraImport::IssueSerializer do
end
let(:priority_field) { { 'name' => 'Medium' } }
- let(:labels_field) { %w(bug dev backend frontend) }
+ let(:labels_field) { %w[bug dev backend frontend] }
let(:fields) do
{
@@ -101,7 +101,7 @@ RSpec.describe Gitlab::JiraImport::IssueSerializer do
end
context 'when there are no new labels' do
- let(:labels_field) { %w(bug dev) }
+ let(:labels_field) { %w[bug dev] }
it 'assigns the labels to the Issue hash' do
expect(subject[:label_ids]).to match_array([project_label.id, group_label.id])
diff --git a/spec/lib/gitlab/jira_import/labels_importer_spec.rb b/spec/lib/gitlab/jira_import/labels_importer_spec.rb
index 4fb5e363475..7579e2c65f4 100644
--- a/spec/lib/gitlab/jira_import/labels_importer_spec.rb
+++ b/spec/lib/gitlab/jira_import/labels_importer_spec.rb
@@ -36,8 +36,8 @@ RSpec.describe Gitlab::JiraImport::LabelsImporter do
let_it_be(:jira_import_with_label) { create(:jira_import_state, label: label, project: project) }
let_it_be(:issue_label) { create(:label, project: project, title: 'bug') }
- let(:jira_labels_1) { { "maxResults" => 2, "startAt" => 0, "total" => 3, "isLast" => false, "values" => %w(backend bug) } }
- let(:jira_labels_2) { { "maxResults" => 2, "startAt" => 2, "total" => 3, "isLast" => true, "values" => %w(feature) } }
+ let(:jira_labels_1) { { "maxResults" => 2, "startAt" => 0, "total" => 3, "isLast" => false, "values" => %w[backend bug] } }
+ let(:jira_labels_2) { { "maxResults" => 2, "startAt" => 2, "total" => 3, "isLast" => true, "values" => %w[feature] } }
context 'when labels are returned from jira' do
before do
@@ -55,8 +55,8 @@ RSpec.describe Gitlab::JiraImport::LabelsImporter do
end
it 'calls Gitlab::JiraImport::HandleLabelsService' do
- expect(Gitlab::JiraImport::HandleLabelsService).to receive(:new).with(project, %w(backend bug)).and_return(double(execute: [1, 2]))
- expect(Gitlab::JiraImport::HandleLabelsService).to receive(:new).with(project, %w(feature)).and_return(double(execute: [3]))
+ expect(Gitlab::JiraImport::HandleLabelsService).to receive(:new).with(project, %w[backend bug]).and_return(double(execute: [1, 2]))
+ expect(Gitlab::JiraImport::HandleLabelsService).to receive(:new).with(project, %w[feature]).and_return(double(execute: [3]))
subject
end
@@ -92,7 +92,7 @@ RSpec.describe Gitlab::JiraImport::LabelsImporter do
end
context 'when the isLast argument is missing' do
- let(:jira_labels) { { "maxResults" => 2, "startAt" => 0, "total" => 3, "values" => %w(bug dev) } }
+ let(:jira_labels) { { "maxResults" => 2, "startAt" => 0, "total" => 3, "values" => %w[bug dev] } }
it_behaves_like 'no labels handling'
end
diff --git a/spec/lib/gitlab/job_waiter_spec.rb b/spec/lib/gitlab/job_waiter_spec.rb
index b000f55e739..e3d7d59df04 100644
--- a/spec/lib/gitlab/job_waiter_spec.rb
+++ b/spec/lib/gitlab/job_waiter_spec.rb
@@ -62,6 +62,10 @@ RSpec.describe Gitlab::JobWaiter, :redis, feature_category: :shared do
before do
allow_any_instance_of(described_class).to receive(:wait).and_call_original
+ stub_feature_flags(
+ use_primary_and_secondary_stores_for_shared_state: false,
+ use_primary_store_as_default_for_shared_state: false
+ )
end
it 'returns when all jobs have been completed' do
@@ -83,36 +87,54 @@ RSpec.describe Gitlab::JobWaiter, :redis, feature_category: :shared do
expect(result).to contain_exactly('a')
end
- context 'when a label is provided' do
- let(:waiter) { described_class.new(2, worker_label: 'Foo') }
- let(:started_total) { double(:started_total) }
- let(:timeouts_total) { double(:timeouts_total) }
+ context 'when migration is ongoing' do
+ let(:waiter) { described_class.new(3) }
- before do
- allow(Gitlab::Metrics).to receive(:counter)
- .with(described_class::STARTED_METRIC, anything)
- .and_return(started_total)
+ shared_examples 'returns all jobs' do
+ it 'returns all jobs' do
+ result = nil
+ expect { Timeout.timeout(6) { result = waiter.wait(5) } }.not_to raise_error
- allow(Gitlab::Metrics).to receive(:counter)
- .with(described_class::TIMEOUTS_METRIC, anything)
- .and_return(timeouts_total)
+ expect(result).to contain_exactly('a', 'b', 'c')
+ end
end
- it 'increments just job_waiter_started_total when all jobs complete' do
- expect(started_total).to receive(:increment).with(worker: 'Foo')
- expect(timeouts_total).not_to receive(:increment)
+ context 'when using both stores' do
+ context 'with existing jobs in old store' do
+ before do
+ described_class.notify(waiter.key, 'a')
+ described_class.notify(waiter.key, 'b')
+ described_class.notify(waiter.key, 'c')
+ stub_feature_flags(use_primary_and_secondary_stores_for_shared_state: true)
+ end
- described_class.notify(waiter.key, 'a')
- described_class.notify(waiter.key, 'b')
+ it_behaves_like 'returns all jobs'
+ end
- expect { Timeout.timeout(1) { waiter.wait(2) } }.not_to raise_error
- end
+ context 'with jobs in both stores' do
+ before do
+ stub_feature_flags(use_primary_and_secondary_stores_for_shared_state: true)
+ described_class.notify(waiter.key, 'a')
+ described_class.notify(waiter.key, 'b')
+ described_class.notify(waiter.key, 'c')
+ end
- it 'increments job_waiter_started_total and job_waiter_timeouts_total when it times out' do
- expect(started_total).to receive(:increment).with(worker: 'Foo')
- expect(timeouts_total).to receive(:increment).with(worker: 'Foo')
+ it_behaves_like 'returns all jobs'
+ end
- expect { Timeout.timeout(2) { waiter.wait(1) } }.not_to raise_error
+ context 'when using primary store as default store' do
+ before do
+ stub_feature_flags(
+ use_primary_and_secondary_stores_for_shared_state: true,
+ use_primary_store_as_default_for_shared_state: true
+ )
+ described_class.notify(waiter.key, 'a')
+ described_class.notify(waiter.key, 'b')
+ described_class.notify(waiter.key, 'c')
+ end
+
+ it_behaves_like 'returns all jobs'
+ end
end
end
end
diff --git a/spec/lib/gitlab/kubernetes/kubectl_cmd_spec.rb b/spec/lib/gitlab/kubernetes/kubectl_cmd_spec.rb
index 3028e0a13aa..f88f7c4c108 100644
--- a/spec/lib/gitlab/kubernetes/kubectl_cmd_spec.rb
+++ b/spec/lib/gitlab/kubernetes/kubectl_cmd_spec.rb
@@ -7,7 +7,7 @@ require_relative '../../../../lib/gitlab/kubernetes/pod_cmd'
RSpec.describe Gitlab::Kubernetes::KubectlCmd do
describe '.delete' do
it 'constructs string properly' do
- args = %w(resource_type type --flag-1 --flag-2)
+ args = %w[resource_type type --flag-1 --flag-2]
expected_command = 'kubectl delete resource_type type --flag-1 --flag-2'
@@ -31,7 +31,7 @@ RSpec.describe Gitlab::Kubernetes::KubectlCmd do
context 'with optional args' do
it 'constructs command properly with many args' do
- args = %w(arg-1 --flag-0-1 arg-2 --flag-0-2)
+ args = %w[arg-1 --flag-0-1 arg-2 --flag-0-2]
expected_command = 'kubectl apply -f filename arg-1 --flag-0-1 arg-2 --flag-0-2'
diff --git a/spec/lib/gitlab/kubernetes/role_spec.rb b/spec/lib/gitlab/kubernetes/role_spec.rb
index acb9b5d4e8e..288a5406372 100644
--- a/spec/lib/gitlab/kubernetes/role_spec.rb
+++ b/spec/lib/gitlab/kubernetes/role_spec.rb
@@ -9,9 +9,9 @@ RSpec.describe Gitlab::Kubernetes::Role do
let(:rules) do
[{
- apiGroups: %w(hello.world),
- resources: %w(oil diamonds coffee),
- verbs: %w(say do walk run)
+ apiGroups: %w[hello.world],
+ resources: %w[oil diamonds coffee],
+ verbs: %w[say do walk run]
}]
end
diff --git a/spec/lib/gitlab/language_data_spec.rb b/spec/lib/gitlab/language_data_spec.rb
index bb4b0c3855c..828fd95f78e 100644
--- a/spec/lib/gitlab/language_data_spec.rb
+++ b/spec/lib/gitlab/language_data_spec.rb
@@ -15,7 +15,7 @@ RSpec.describe Gitlab::LanguageData do
expect(described_class.extensions).to be_a(Set)
expect(described_class.extensions.count).to be > 0
# Sanity check for known extensions
- expect(described_class.extensions).to include(*%w(.rb .yml .json))
+ expect(described_class.extensions).to include(*%w[.rb .yml .json])
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 7259b5e2484..d3ddf034cd3 100644
--- a/spec/lib/gitlab/mail_room/mail_room_spec.rb
+++ b/spec/lib/gitlab/mail_room/mail_room_spec.rb
@@ -245,7 +245,6 @@ RSpec.describe Gitlab::MailRoom, feature_category: :build do
delivery_options: {
redis_url: "localhost",
redis_db: 99,
- namespace: "resque:gitlab",
queue: "default",
worker: "EmailReceiverWorker",
sentinels: [{ host: "localhost", port: 1234 }]
@@ -258,7 +257,6 @@ RSpec.describe Gitlab::MailRoom, feature_category: :build do
delivery_options: {
redis_url: "localhost",
redis_db: 99,
- namespace: "resque:gitlab",
queue: "default",
worker: "ServiceDeskEmailReceiverWorker",
sentinels: [{ host: "localhost", port: 1234 }]
diff --git a/spec/lib/gitlab/markup_helper_spec.rb b/spec/lib/gitlab/markup_helper_spec.rb
index 2bffd029568..a7508288f4e 100644
--- a/spec/lib/gitlab/markup_helper_spec.rb
+++ b/spec/lib/gitlab/markup_helper_spec.rb
@@ -4,8 +4,8 @@ require 'fast_spec_helper'
RSpec.describe Gitlab::MarkupHelper do
describe '#markup?' do
- %w(textile rdoc org creole wiki
- mediawiki rst adoc ad asciidoc mdown md markdown).each do |type|
+ %w[textile rdoc org creole wiki
+ mediawiki rst adoc ad asciidoc mdown md markdown].each do |type|
it "returns true for #{type} files" do
expect(described_class.markup?("README.#{type}")).to be_truthy
end
@@ -17,7 +17,7 @@ RSpec.describe Gitlab::MarkupHelper do
end
describe '#gitlab_markdown?' do
- %w(mdown mkd mkdn md markdown).each do |type|
+ %w[mdown mkd mkdn md markdown].each do |type|
it "returns true for #{type} files" do
expect(described_class.gitlab_markdown?("README.#{type}")).to be_truthy
end
@@ -29,7 +29,7 @@ RSpec.describe Gitlab::MarkupHelper do
end
describe '#asciidoc?' do
- %w(adoc ad asciidoc ADOC).each do |type|
+ %w[adoc ad asciidoc ADOC].each do |type|
it "returns true for #{type} files" do
expect(described_class.asciidoc?("README.#{type}")).to be_truthy
end
diff --git a/spec/lib/gitlab/memory/instrumentation_spec.rb b/spec/lib/gitlab/memory/instrumentation_spec.rb
index f287edb7da3..059bcad37e7 100644
--- a/spec/lib/gitlab/memory/instrumentation_spec.rb
+++ b/spec/lib/gitlab/memory/instrumentation_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::Memory::Instrumentation, feature_category: :application_performance do
+RSpec.describe Gitlab::Memory::Instrumentation, feature_category: :cloud_connector do
include MemoryInstrumentationHelper
before do
diff --git a/spec/lib/gitlab/memory/reporter_spec.rb b/spec/lib/gitlab/memory/reporter_spec.rb
index 1d19d7129cf..5ba429ae6bc 100644
--- a/spec/lib/gitlab/memory/reporter_spec.rb
+++ b/spec/lib/gitlab/memory/reporter_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::Memory::Reporter, :aggregate_failures, feature_category: :application_performance do
+RSpec.describe Gitlab::Memory::Reporter, :aggregate_failures, feature_category: :cloud_connector do
let(:fake_report) do
Class.new do
def name
diff --git a/spec/lib/gitlab/memory/reports/heap_dump_spec.rb b/spec/lib/gitlab/memory/reports/heap_dump_spec.rb
index 4e235a71bdb..7888f8b0c79 100644
--- a/spec/lib/gitlab/memory/reports/heap_dump_spec.rb
+++ b/spec/lib/gitlab/memory/reports/heap_dump_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::Memory::Reports::HeapDump, feature_category: :application_performance do
+RSpec.describe Gitlab::Memory::Reports::HeapDump, feature_category: :cloud_connector do
# Copy this class so we do not mess with its state.
let(:klass) { described_class.dup }
diff --git a/spec/lib/gitlab/memory/watchdog/configurator_spec.rb b/spec/lib/gitlab/memory/watchdog/configurator_spec.rb
index 035652abfe6..cd9ac0d7a8d 100644
--- a/spec/lib/gitlab/memory/watchdog/configurator_spec.rb
+++ b/spec/lib/gitlab/memory/watchdog/configurator_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::Memory::Watchdog::Configurator, feature_category: :application_performance do
+RSpec.describe Gitlab::Memory::Watchdog::Configurator, feature_category: :cloud_connector do
shared_examples 'as configurator' do |handler_class, event_reporter_class, sleep_time_env, sleep_time|
it 'configures the correct handler' do
configurator.call(configuration)
diff --git a/spec/lib/gitlab/memory/watchdog/event_reporter_spec.rb b/spec/lib/gitlab/memory/watchdog/event_reporter_spec.rb
index f1d241249e2..e27f842bc71 100644
--- a/spec/lib/gitlab/memory/watchdog/event_reporter_spec.rb
+++ b/spec/lib/gitlab/memory/watchdog/event_reporter_spec.rb
@@ -3,7 +3,7 @@
require 'fast_spec_helper'
require 'prometheus/client'
-RSpec.describe Gitlab::Memory::Watchdog::EventReporter, feature_category: :application_performance do
+RSpec.describe Gitlab::Memory::Watchdog::EventReporter, feature_category: :cloud_connector do
let(:logger) { instance_double(::Logger) }
let(:violations_counter) { instance_double(::Prometheus::Client::Counter) }
let(:violations_handled_counter) { instance_double(::Prometheus::Client::Counter) }
diff --git a/spec/lib/gitlab/memory/watchdog/handlers/null_handler_spec.rb b/spec/lib/gitlab/memory/watchdog/handlers/null_handler_spec.rb
index 09c76de9611..96cb02393f9 100644
--- a/spec/lib/gitlab/memory/watchdog/handlers/null_handler_spec.rb
+++ b/spec/lib/gitlab/memory/watchdog/handlers/null_handler_spec.rb
@@ -2,7 +2,7 @@
require 'fast_spec_helper'
-RSpec.describe Gitlab::Memory::Watchdog::Handlers::NullHandler, feature_category: :application_performance do
+RSpec.describe Gitlab::Memory::Watchdog::Handlers::NullHandler, feature_category: :cloud_connector do
subject(:handler) { described_class.instance }
describe '#call' do
diff --git a/spec/lib/gitlab/memory/watchdog/handlers/puma_handler_spec.rb b/spec/lib/gitlab/memory/watchdog/handlers/puma_handler_spec.rb
index 7df95c1722e..4c2fd5a3283 100644
--- a/spec/lib/gitlab/memory/watchdog/handlers/puma_handler_spec.rb
+++ b/spec/lib/gitlab/memory/watchdog/handlers/puma_handler_spec.rb
@@ -2,7 +2,7 @@
require 'fast_spec_helper'
-RSpec.describe Gitlab::Memory::Watchdog::Handlers::PumaHandler, feature_category: :application_performance do
+RSpec.describe Gitlab::Memory::Watchdog::Handlers::PumaHandler, feature_category: :cloud_connector do
# rubocop: disable RSpec/VerifiedDoubles
# In tests, the Puma constant is not loaded so we cannot make this an instance_double.
let(:puma_worker_handle_class) { double('Puma::Cluster::WorkerHandle') }
diff --git a/spec/lib/gitlab/memory/watchdog/handlers/sidekiq_handler_spec.rb b/spec/lib/gitlab/memory/watchdog/handlers/sidekiq_handler_spec.rb
index d1f303e7731..68dd784fb7e 100644
--- a/spec/lib/gitlab/memory/watchdog/handlers/sidekiq_handler_spec.rb
+++ b/spec/lib/gitlab/memory/watchdog/handlers/sidekiq_handler_spec.rb
@@ -3,7 +3,7 @@
require 'fast_spec_helper'
require 'sidekiq'
-RSpec.describe Gitlab::Memory::Watchdog::Handlers::SidekiqHandler, feature_category: :application_performance do
+RSpec.describe Gitlab::Memory::Watchdog::Handlers::SidekiqHandler, feature_category: :cloud_connector do
let(:sleep_time) { 3 }
let(:shutdown_timeout_seconds) { 30 }
let(:handler_iterations) { 0 }
diff --git a/spec/lib/gitlab/memory/watchdog/monitor/rss_memory_limit_spec.rb b/spec/lib/gitlab/memory/watchdog/monitor/rss_memory_limit_spec.rb
index 67d185fd2f1..552736a55ef 100644
--- a/spec/lib/gitlab/memory/watchdog/monitor/rss_memory_limit_spec.rb
+++ b/spec/lib/gitlab/memory/watchdog/monitor/rss_memory_limit_spec.rb
@@ -4,7 +4,7 @@ require 'fast_spec_helper'
require 'prometheus/client'
require 'support/shared_examples/lib/gitlab/memory/watchdog/monitor_result_shared_examples'
-RSpec.describe Gitlab::Memory::Watchdog::Monitor::RssMemoryLimit, feature_category: :application_performance do
+RSpec.describe Gitlab::Memory::Watchdog::Monitor::RssMemoryLimit, feature_category: :cloud_connector do
let(:max_rss_limit_gauge) { instance_double(::Prometheus::Client::Gauge) }
let(:memory_limit_bytes) { 2_097_152_000 }
let(:worker_memory_bytes) { 1_048_576_000 }
diff --git a/spec/lib/gitlab/memory/watchdog/sidekiq_event_reporter_spec.rb b/spec/lib/gitlab/memory/watchdog/sidekiq_event_reporter_spec.rb
index 48595c3f172..06b1646d418 100644
--- a/spec/lib/gitlab/memory/watchdog/sidekiq_event_reporter_spec.rb
+++ b/spec/lib/gitlab/memory/watchdog/sidekiq_event_reporter_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::Memory::Watchdog::SidekiqEventReporter, feature_category: :application_performance do
+RSpec.describe Gitlab::Memory::Watchdog::SidekiqEventReporter, feature_category: :cloud_connector do
let(:counter) { instance_double(::Prometheus::Client::Counter) }
before do
diff --git a/spec/lib/gitlab/memory/watchdog_spec.rb b/spec/lib/gitlab/memory/watchdog_spec.rb
index dd6bfb6da2c..c442208617f 100644
--- a/spec/lib/gitlab/memory/watchdog_spec.rb
+++ b/spec/lib/gitlab/memory/watchdog_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::Memory::Watchdog, :aggregate_failures, feature_category: :application_performance do
+RSpec.describe Gitlab::Memory::Watchdog, :aggregate_failures, feature_category: :cloud_connector do
context 'watchdog' do
let(:configuration) { instance_double(described_class::Configuration) }
let(:handler) { instance_double(described_class::Handlers::NullHandler) }
diff --git a/spec/lib/gitlab/merge_requests/mergeability/check_result_spec.rb b/spec/lib/gitlab/merge_requests/mergeability/check_result_spec.rb
index 74aa3528328..cd84525f7e5 100644
--- a/spec/lib/gitlab/merge_requests/mergeability/check_result_spec.rb
+++ b/spec/lib/gitlab/merge_requests/mergeability/check_result_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::MergeRequests::Mergeability::CheckResult do
+RSpec.describe Gitlab::MergeRequests::Mergeability::CheckResult, feature_category: :code_review_workflow do
subject(:check_result) { described_class }
let(:time) { Time.current }
@@ -63,6 +63,28 @@ RSpec.describe Gitlab::MergeRequests::Mergeability::CheckResult do
end
end
+ describe '.inactive' do
+ subject(:inactive) { check_result.inactive(payload: payload) }
+
+ let(:payload) { {} }
+
+ it 'creates a inactive result' do
+ expect(inactive.status).to eq described_class::INACTIVE_STATUS
+ end
+
+ it 'uses the default payload' do
+ expect(inactive.payload).to eq described_class.default_payload
+ end
+
+ context 'when given a payload' do
+ let(:payload) { { last_run_at: time + 1.day, test: 'test' } }
+
+ it 'uses the payload passed' do
+ expect(inactive.payload).to eq payload
+ end
+ end
+ end
+
describe '.from_hash' do
subject(:from_hash) { described_class.from_hash(hash) }
diff --git a/spec/lib/gitlab/metrics/exporter/base_exporter_spec.rb b/spec/lib/gitlab/metrics/exporter/base_exporter_spec.rb
index 6673cc50d67..4184c674823 100644
--- a/spec/lib/gitlab/metrics/exporter/base_exporter_spec.rb
+++ b/spec/lib/gitlab/metrics/exporter/base_exporter_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::Metrics::Exporter::BaseExporter, feature_category: :application_performance do
+RSpec.describe Gitlab::Metrics::Exporter::BaseExporter, feature_category: :cloud_connector do
let(:settings) { double('settings') }
let(:log_enabled) { false }
let(:exporter) { described_class.new(settings, log_enabled: log_enabled, log_file: 'test_exporter.log') }
diff --git a/spec/lib/gitlab/metrics/rails_slis_spec.rb b/spec/lib/gitlab/metrics/rails_slis_spec.rb
index ef996f61082..3050c769117 100644
--- a/spec/lib/gitlab/metrics/rails_slis_spec.rb
+++ b/spec/lib/gitlab/metrics/rails_slis_spec.rb
@@ -3,7 +3,7 @@ require 'spec_helper'
RSpec.describe Gitlab::Metrics::RailsSlis, feature_category: :error_budgets do
before do
- allow(Gitlab::Graphql::KnownOperations).to receive(:default).and_return(Gitlab::Graphql::KnownOperations.new(%w(foo bar)))
+ allow(Gitlab::Graphql::KnownOperations).to receive(:default).and_return(Gitlab::Graphql::KnownOperations.new(%w[foo bar]))
end
describe '.initialize_request_slis!' do
diff --git a/spec/lib/gitlab/metrics/samplers/threads_sampler_spec.rb b/spec/lib/gitlab/metrics/samplers/threads_sampler_spec.rb
index 5dabafb7c0b..0a3648c8b9a 100644
--- a/spec/lib/gitlab/metrics/samplers/threads_sampler_spec.rb
+++ b/spec/lib/gitlab/metrics/samplers/threads_sampler_spec.rb
@@ -40,11 +40,11 @@ RSpec.describe Gitlab::Metrics::Samplers::ThreadsSampler do
context 'thread names', :aggregate_failures do
where(:thread_names, :expected_names) do
[
- [[nil], %w(unnamed)],
+ [[nil], %w[unnamed]],
[['puma threadpool 1', 'puma threadpool 001', 'puma threadpool 002'], ['puma threadpool']],
- [%w(sidekiq_worker_thread), %w(sidekiq_worker_thread)],
- [%w(some_sampler some_exporter), %w(some_sampler some_exporter)],
- [%w(unknown thing), %w(unrecognized)]
+ [%w[sidekiq_worker_thread], %w[sidekiq_worker_thread]],
+ [%w[some_sampler some_exporter], %w[some_sampler some_exporter]],
+ [%w[unknown thing], %w[unrecognized]]
]
end
diff --git a/spec/lib/gitlab/metrics/subscribers/action_cable_spec.rb b/spec/lib/gitlab/metrics/subscribers/action_cable_spec.rb
index 54868bb6ca4..8fbfcdc3bd5 100644
--- a/spec/lib/gitlab/metrics/subscribers/action_cable_spec.rb
+++ b/spec/lib/gitlab/metrics/subscribers/action_cable_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::Metrics::Subscribers::ActionCable, :request_store, feature_category: :application_performance do
+RSpec.describe Gitlab::Metrics::Subscribers::ActionCable, :request_store, feature_category: :cloud_connector do
let(:subscriber) { described_class.new }
let(:counter) { double(:counter) }
let(:transmitted_bytes_counter) { double(:counter) }
diff --git a/spec/lib/gitlab/middleware/go_spec.rb b/spec/lib/gitlab/middleware/go_spec.rb
index a3835f9eed0..4820f42ade3 100644
--- a/spec/lib/gitlab/middleware/go_spec.rb
+++ b/spec/lib/gitlab/middleware/go_spec.rb
@@ -206,7 +206,7 @@ RSpec.describe Gitlab::Middleware::Go, feature_category: :source_code_management
expect(response[0]).to eq(404)
expect(response[1]['Content-Type']).to eq('text/html')
- expected_body = %{<html><body>go get #{Gitlab.config.gitlab.url}/#{project.full_path}</body></html>}
+ expected_body = %(<html><body>go get #{Gitlab.config.gitlab.url}/#{project.full_path}</body></html>)
expect(response[2]).to eq([expected_body])
end
end
@@ -278,7 +278,7 @@ RSpec.describe Gitlab::Middleware::Go, feature_category: :source_code_management
project_url = "http://#{Gitlab.config.gitlab.host}/#{path}"
expect(response[0]).to eq(200)
expect(response[1]['Content-Type']).to eq('text/html')
- expected_body = %{<html><head><meta name="go-import" content="#{Gitlab.config.gitlab.host}/#{path} git #{repository_url}"><meta name="go-source" content="#{Gitlab.config.gitlab.host}/#{path} #{project_url} #{project_url}/-/tree/#{branch}{/dir} #{project_url}/-/blob/#{branch}{/dir}/{file}#L{line}"></head><body>go get #{Gitlab.config.gitlab.url}/#{path}</body></html>}
+ expected_body = %(<html><head><meta name="go-import" content="#{Gitlab.config.gitlab.host}/#{path} git #{repository_url}"><meta name="go-source" content="#{Gitlab.config.gitlab.host}/#{path} #{project_url} #{project_url}/-/tree/#{branch}{/dir} #{project_url}/-/blob/#{branch}{/dir}/{file}#L{line}"></head><body>go get #{Gitlab.config.gitlab.url}/#{path}</body></html>)
expect(response[2]).to eq([expected_body])
end
end
diff --git a/spec/lib/gitlab/middleware/multipart_spec.rb b/spec/lib/gitlab/middleware/multipart_spec.rb
index 509a4bb921b..b857ed47d42 100644
--- a/spec/lib/gitlab/middleware/multipart_spec.rb
+++ b/spec/lib/gitlab/middleware/multipart_spec.rb
@@ -33,7 +33,7 @@ RSpec.describe Gitlab::Middleware::Multipart do
let(:params) { upload_parameters_for(key: 'file', mode: mode, filename: filename, remote_id: remote_id).merge('file.path' => '/should/not/be/read') }
it 'builds an UploadedFile' do
- expect_uploaded_files(original_filename: filename, remote_id: remote_id, size: uploaded_file.size, params_path: %w(file))
+ expect_uploaded_files(original_filename: filename, remote_id: remote_id, size: uploaded_file.size, params_path: %w[file])
subject
end
@@ -61,7 +61,7 @@ RSpec.describe Gitlab::Middleware::Multipart do
let(:params) { upload_parameters_for(filepath: uploaded_filepath, key: 'file', mode: mode, filename: filename) }
it 'builds an UploadedFile' do
- expect_uploaded_files(filepath: uploaded_filepath, original_filename: filename, size: uploaded_file.size, params_path: %w(file))
+ expect_uploaded_files(filepath: uploaded_filepath, original_filename: filename, size: uploaded_file.size, params_path: %w[file])
subject
end
diff --git a/spec/lib/gitlab/middleware/path_traversal_check_spec.rb b/spec/lib/gitlab/middleware/path_traversal_check_spec.rb
index 3d334a60c49..91081cc88ea 100644
--- a/spec/lib/gitlab/middleware/path_traversal_check_spec.rb
+++ b/spec/lib/gitlab/middleware/path_traversal_check_spec.rb
@@ -55,6 +55,34 @@ RSpec.describe ::Gitlab::Middleware::PathTraversalCheck, feature_category: :shar
end
end
+ shared_examples 'excluded path' do
+ it 'measures and logs the execution time' do
+ expect(::Gitlab::PathTraversal)
+ .not_to receive(:check_path_traversal!)
+ expect(::Gitlab::AppLogger)
+ .to receive(:warn)
+ .with({ class_name: described_class.name, duration_ms: instance_of(Float) })
+ .and_call_original
+
+ expect(subject).to eq(fake_response)
+ end
+
+ context 'with log_execution_time_path_traversal_middleware disabled' do
+ before do
+ stub_feature_flags(log_execution_time_path_traversal_middleware: false)
+ end
+
+ it 'does nothing' do
+ expect(::Gitlab::PathTraversal)
+ .not_to receive(:check_path_traversal!)
+ expect(::Gitlab::AppLogger)
+ .not_to receive(:warn)
+
+ expect(subject).to eq(fake_response)
+ end
+ end
+ end
+
shared_examples 'path traversal' do
it 'logs the problem and measures the execution time' do
expect(::Gitlab::PathTraversal)
@@ -70,7 +98,8 @@ RSpec.describe ::Gitlab::Middleware::PathTraversalCheck, feature_category: :shar
class_name: described_class.name,
duration_ms: instance_of(Float),
message: described_class::PATH_TRAVERSAL_MESSAGE,
- fullpath: fullpath
+ fullpath: fullpath,
+ method: method.upcase
}).and_call_original
expect(subject).to eq(fake_response)
@@ -94,7 +123,8 @@ RSpec.describe ::Gitlab::Middleware::PathTraversalCheck, feature_category: :shar
.with({
class_name: described_class.name,
message: described_class::PATH_TRAVERSAL_MESSAGE,
- fullpath: fullpath
+ fullpath: fullpath,
+ method: method.upcase
}).and_call_original
expect(subject).to eq(fake_response)
@@ -110,23 +140,90 @@ RSpec.describe ::Gitlab::Middleware::PathTraversalCheck, feature_category: :shar
let(:method) { 'get' }
where(:path, :query_params, :shared_example_name) do
- '/foo/bar' | {} | 'no issue'
- '/foo/../bar' | {} | 'path traversal'
- '/foo%2Fbar' | {} | 'no issue'
- '/foo%2F..%2Fbar' | {} | 'path traversal'
- '/foo%252F..%252Fbar' | {} | 'no issue'
- '/foo/bar' | { x: 'foo' } | 'no issue'
- '/foo/bar' | { x: 'foo/../bar' } | 'path traversal'
- '/foo/bar' | { x: 'foo%2Fbar' } | 'no issue'
- '/foo/bar' | { x: 'foo%2F..%2Fbar' } | 'no issue'
- '/foo/bar' | { x: 'foo%252F..%252Fbar' } | 'no issue'
- '/foo%2F..%2Fbar' | { x: 'foo%252F..%252Fbar' } | 'path traversal'
+ '/foo/bar' | {} | 'no issue'
+ '/foo/../bar' | {} | 'path traversal'
+ '/foo%2Fbar' | {} | 'no issue'
+ '/foo%2F..%2Fbar' | {} | 'path traversal'
+ '/foo%252F..%252Fbar' | {} | 'no issue'
+
+ '/foo/bar' | { x: 'foo' } | 'no issue'
+ '/foo/bar' | { x: 'foo/../bar' } | 'path traversal'
+ '/foo/bar' | { x: 'foo%2Fbar' } | 'no issue'
+ '/foo/bar' | { x: 'foo%2F..%2Fbar' } | 'no issue'
+ '/foo/bar' | { x: 'foo%252F..%252Fbar' } | 'no issue'
+ '/foo%2F..%2Fbar' | { x: 'foo%252F..%252Fbar' } | 'path traversal'
end
with_them do
it_behaves_like params[:shared_example_name]
end
+ context 'for global search excluded paths' do
+ excluded_paths = %w[
+ /search
+ /search/count
+ /api/v4/search
+ /api/v4/search.json
+ /api/v4/projects/4/search
+ /api/v4/projects/4/search.json
+ /api/v4/projects/4/-/search
+ /api/v4/projects/4/-/search.json
+ /api/v4/projects/my%2Fproject/search
+ /api/v4/projects/my%2Fproject/search.json
+ /api/v4/projects/my%2Fproject/-/search
+ /api/v4/projects/my%2Fproject/-/search.json
+ /api/v4/groups/4/search
+ /api/v4/groups/4/search.json
+ /api/v4/groups/4/-/search
+ /api/v4/groups/4/-/search.json
+ /api/v4/groups/my%2Fgroup/search
+ /api/v4/groups/my%2Fgroup/search.json
+ /api/v4/groups/my%2Fgroup/-/search
+ /api/v4/groups/my%2Fgroup/-/search.json
+ ]
+ query_params_with_no_path_traversal = [
+ {},
+ { x: 'foo' },
+ { x: 'foo%2F..%2Fbar' },
+ { x: 'foo%2F..%2Fbar' },
+ { x: 'foo%252F..%252Fbar' }
+ ]
+ query_params_with_path_traversal = [
+ { x: 'foo/../bar' }
+ ]
+
+ excluded_paths.each do |excluded_path|
+ [query_params_with_no_path_traversal + query_params_with_path_traversal].flatten.each do |qp|
+ context "for excluded path #{excluded_path} with query params #{qp}" do
+ let(:query_params) { qp }
+ let(:path) { excluded_path }
+
+ it_behaves_like 'excluded path'
+ end
+ end
+
+ non_excluded_path = excluded_path.gsub('search', 'searchtest')
+
+ query_params_with_no_path_traversal.each do |qp|
+ context "for non excluded path #{non_excluded_path} with query params #{qp}" do
+ let(:query_params) { qp }
+ let(:path) { non_excluded_path }
+
+ it_behaves_like 'no issue'
+ end
+ end
+
+ query_params_with_path_traversal.each do |qp|
+ context "for non excluded path #{non_excluded_path} with query params #{qp}" do
+ let(:query_params) { qp }
+ let(:path) { non_excluded_path }
+
+ it_behaves_like 'path traversal'
+ end
+ end
+ end
+ end
+
context 'with a issues search path' do
let(:query_params) { {} }
let(:path) do
@@ -147,6 +244,7 @@ RSpec.describe ::Gitlab::Middleware::PathTraversalCheck, feature_category: :shar
'/foo%2Fbar' | {} | 'no issue'
'/foo%2F..%2Fbar' | {} | 'path traversal'
'/foo%252F..%252Fbar' | {} | 'no issue'
+
'/foo/bar' | { x: 'foo' } | 'no issue'
'/foo/bar' | { x: 'foo/../bar' } | 'no issue'
'/foo/bar' | { x: 'foo%2Fbar' } | 'no issue'
@@ -158,6 +256,59 @@ RSpec.describe ::Gitlab::Middleware::PathTraversalCheck, feature_category: :shar
with_them do
it_behaves_like params[:shared_example_name]
end
+
+ context 'for global search excluded paths' do
+ excluded_paths = %w[
+ /search
+ /search/count
+ /api/v4/search
+ /api/v4/search.json
+ /api/v4/projects/4/search
+ /api/v4/projects/4/search.json
+ /api/v4/projects/4/-/search
+ /api/v4/projects/4/-/search.json
+ /api/v4/projects/my%2Fproject/search
+ /api/v4/projects/my%2Fproject/search.json
+ /api/v4/projects/my%2Fproject/-/search
+ /api/v4/projects/my%2Fproject/-/search.json
+ /api/v4/groups/4/search
+ /api/v4/groups/4/search.json
+ /api/v4/groups/4/-/search
+ /api/v4/groups/4/-/search.json
+ /api/v4/groups/my%2Fgroup/search
+ /api/v4/groups/my%2Fgroup/search.json
+ /api/v4/groups/my%2Fgroup/-/search
+ /api/v4/groups/my%2Fgroup/-/search.json
+ ]
+ all_query_params = [
+ {},
+ { x: 'foo' },
+ { x: 'foo%2F..%2Fbar' },
+ { x: 'foo%2F..%2Fbar' },
+ { x: 'foo%252F..%252Fbar' },
+ { x: 'foo/../bar' }
+ ]
+
+ excluded_paths.each do |excluded_path|
+ all_query_params.each do |qp|
+ context "for excluded path #{excluded_path} with query params #{qp}" do
+ let(:query_params) { qp }
+ let(:path) { excluded_path }
+
+ it_behaves_like 'excluded path'
+ end
+
+ non_excluded_path = excluded_path.gsub('search', 'searchtest')
+
+ context "for non excluded path #{non_excluded_path} with query params #{qp}" do
+ let(:query_params) { qp }
+ let(:path) { excluded_path.gsub('search', 'searchtest') }
+
+ it_behaves_like 'no issue'
+ end
+ end
+ end
+ end
end
end
@@ -177,6 +328,12 @@ RSpec.describe ::Gitlab::Middleware::PathTraversalCheck, feature_category: :shar
'/foo/bar' | { x: 'foo%2Fbar' }
'/foo/bar' | { x: 'foo%2F..%2Fbar' }
'/foo/bar' | { x: 'foo%252F..%252Fbar' }
+ '/search' | { x: 'foo/../bar' }
+ '/search' | { x: 'foo%2F..%2Fbar' }
+ '/search' | { x: 'foo%252F..%252Fbar' }
+ '%2Fsearch' | { x: 'foo/../bar' }
+ '%2Fsearch' | { x: 'foo%2F..%2Fbar' }
+ '%2Fsearch' | { x: 'foo%252F..%252Fbar' }
end
with_them do
diff --git a/spec/lib/gitlab/omniauth_initializer_spec.rb b/spec/lib/gitlab/omniauth_initializer_spec.rb
index 1c665ec6e18..e12c0f4e78b 100644
--- a/spec/lib/gitlab/omniauth_initializer_spec.rb
+++ b/spec/lib/gitlab/omniauth_initializer_spec.rb
@@ -2,7 +2,9 @@
require 'spec_helper'
-RSpec.describe Gitlab::OmniauthInitializer do
+RSpec.describe Gitlab::OmniauthInitializer, feature_category: :system_access do
+ include LoginHelpers
+
let(:devise_config) { class_double(Devise) }
subject(:initializer) { described_class.new(devise_config) }
@@ -171,7 +173,7 @@ RSpec.describe Gitlab::OmniauthInitializer do
end
it 'allows "args" array for app_id and app_secret' do
- legacy_config = { 'name' => 'legacy', 'args' => %w(123 abc) }
+ legacy_config = { 'name' => 'legacy', 'args' => %w[123 abc] }
expect(devise_config).to receive(:omniauth).with(:legacy, '123', 'abc')
@@ -224,6 +226,119 @@ RSpec.describe Gitlab::OmniauthInitializer do
subject.execute([shibboleth_config])
end
+ context 'when SAML providers are configured' do
+ it 'configures default args for a single SAML provider' do
+ stub_omniauth_config(providers: [{ name: 'saml', args: { idp_sso_service_url: 'https://saml.example.com' } }])
+
+ expect(devise_config).to receive(:omniauth).with(
+ :saml,
+ {
+ idp_sso_service_url: 'https://saml.example.com',
+ attribute_statements: ::Gitlab::Auth::Saml::Config.default_attribute_statements
+ }
+ )
+
+ initializer.execute(Gitlab.config.omniauth.providers)
+ end
+
+ context 'when configuration provides matching keys' do
+ before do
+ stub_omniauth_config(
+ providers: [
+ {
+ name: 'saml',
+ args: { idp_sso_service_url: 'https://saml.example.com', attribute_statements: { email: ['custom_attr'] } }
+ }
+ ]
+ )
+ end
+
+ it 'merges arguments with user configuration preference' do
+ expect(devise_config).to receive(:omniauth).with(
+ :saml,
+ {
+ idp_sso_service_url: 'https://saml.example.com',
+ attribute_statements: ::Gitlab::Auth::Saml::Config.default_attribute_statements
+ .merge({ email: ['custom_attr'] })
+ }
+ )
+
+ initializer.execute(Gitlab.config.omniauth.providers)
+ end
+
+ it 'merges arguments with defaults preference when invert_omniauth_args_merging is not enabled' do
+ stub_feature_flags(invert_omniauth_args_merging: false)
+
+ expect(devise_config).to receive(:omniauth).with(
+ :saml,
+ {
+ idp_sso_service_url: 'https://saml.example.com',
+ attribute_statements: ::Gitlab::Auth::Saml::Config.default_attribute_statements
+ }
+ )
+
+ initializer.execute(Gitlab.config.omniauth.providers)
+ end
+ end
+
+ it 'configures defaults args for multiple SAML providers' do
+ stub_omniauth_config(
+ providers: [
+ { name: 'saml', args: { idp_sso_service_url: 'https://saml.example.com' } },
+ {
+ name: 'saml2',
+ args: { strategy_class: 'OmniAuth::Strategies::SAML', idp_sso_service_url: 'https://saml2.example.com' }
+ }
+ ]
+ )
+
+ expect(devise_config).to receive(:omniauth).with(
+ :saml,
+ {
+ idp_sso_service_url: 'https://saml.example.com',
+ attribute_statements: ::Gitlab::Auth::Saml::Config.default_attribute_statements
+ }
+ )
+ expect(devise_config).to receive(:omniauth).with(
+ :saml2,
+ {
+ idp_sso_service_url: 'https://saml2.example.com',
+ strategy_class: OmniAuth::Strategies::SAML,
+ attribute_statements: ::Gitlab::Auth::Saml::Config.default_attribute_statements
+ }
+ )
+
+ initializer.execute(Gitlab.config.omniauth.providers)
+ end
+
+ it 'merges arguments with user configuration preference for custom SAML provider' do
+ stub_omniauth_config(
+ providers: [
+ {
+ name: 'custom_saml',
+ args: {
+ strategy_class: 'OmniAuth::Strategies::SAML',
+ idp_sso_service_url: 'https://saml2.example.com',
+ attribute_statements: { email: ['custom_attr'] }
+ }
+ }
+ ]
+ )
+
+ expect(devise_config).to receive(:omniauth).with(
+ :custom_saml,
+ {
+ idp_sso_service_url: 'https://saml2.example.com',
+ strategy_class: OmniAuth::Strategies::SAML,
+ attribute_statements: ::Gitlab::Auth::Saml::Config.default_attribute_statements
+ .merge({ email: ['custom_attr'] })
+ }
+ )
+
+ initializer.execute(Gitlab.config.omniauth.providers)
+ end
+ end
+
it 'configures defaults for google_oauth2' do
google_config = {
'name' => 'google_oauth2',
diff --git a/spec/lib/gitlab/other_markup_spec.rb b/spec/lib/gitlab/other_markup_spec.rb
index 34f1e0cfbc5..266cb75a8ac 100644
--- a/spec/lib/gitlab/other_markup_spec.rb
+++ b/spec/lib/gitlab/other_markup_spec.rb
@@ -59,6 +59,31 @@ RSpec.describe Gitlab::OtherMarkup, feature_category: :wiki do
end
end
+ context 'when mediawiki content' do
+ links = {
+ 'p' => {
+ file: 'file.mediawiki',
+ input: 'Red Bridge (JRuby Embed)',
+ output: "\n<p>Red Bridge (JRuby Embed)</p>"
+ },
+ 'h1' => {
+ file: 'file.mediawiki',
+ input: '= Red Bridge (JRuby Embed) =',
+ output: "\n\n<h1>\n<a name=\"Red_Bridge_JRuby_Embed\"></a><span>Red Bridge (JRuby Embed)</span>\n</h1>\n"
+ },
+ 'h2' => {
+ file: 'file.mediawiki',
+ input: '== Red Bridge (JRuby Embed) ==',
+ output: "\n\n<h2>\n<a name=\"Red_Bridge_JRuby_Embed\"></a><span>Red Bridge (JRuby Embed)</span>\n</h2>\n"
+ }
+ }
+ links.each do |name, data|
+ it "does render into #{name} element" do
+ expect(render(data[:file], data[:input], context)).to eq(data[:output])
+ end
+ end
+ end
+
context 'when rendering takes too long' do
let_it_be(:file_name) { 'foo.bar' }
let_it_be(:project) { create(:project, :repository) }
@@ -86,6 +111,24 @@ RSpec.describe Gitlab::OtherMarkup, feature_category: :wiki do
end
end
+ context 'RedCloth markup' do
+ it 'renders textile correctly' do
+ test_text = '"This is *my* text."'
+ expected_res = "<p>&#8220;This is <strong>my</strong> text.&#8221;</p>"
+ expect(RedCloth.new(test_text).to_html).to eq(expected_res)
+ end
+
+ it 'protects against malicious backtracking' do
+ test_text = '<A' + ('A' * 54773)
+
+ expect do
+ Timeout.timeout(Gitlab::OtherMarkup::RENDER_TIMEOUT.seconds) do
+ RedCloth.new(test_text, [:sanitize_html]).to_html
+ end
+ end.not_to raise_error
+ end
+ end
+
def render(...)
described_class.render(...)
end
diff --git a/spec/lib/gitlab/pages/deployment_update_spec.rb b/spec/lib/gitlab/pages/deployment_update_spec.rb
index cf109248f36..9a7564ddd59 100644
--- a/spec/lib/gitlab/pages/deployment_update_spec.rb
+++ b/spec/lib/gitlab/pages/deployment_update_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::Pages::DeploymentUpdate do
+RSpec.describe Gitlab::Pages::DeploymentUpdate, feature_category: :pages do
let_it_be(:project, refind: true) { create(:project, :repository) }
let_it_be(:old_pipeline) { create(:ci_pipeline, project: project, sha: project.commit('HEAD').sha) }
diff --git a/spec/lib/gitlab/pages/virtual_host_finder_spec.rb b/spec/lib/gitlab/pages/virtual_host_finder_spec.rb
index 8c34968bbfc..bc3f9d89b5f 100644
--- a/spec/lib/gitlab/pages/virtual_host_finder_spec.rb
+++ b/spec/lib/gitlab/pages/virtual_host_finder_spec.rb
@@ -5,10 +5,6 @@ require 'spec_helper'
RSpec.describe Gitlab::Pages::VirtualHostFinder, feature_category: :pages do
let_it_be(:project) { create(:project) }
- before_all do
- project.update_pages_deployment!(create(:pages_deployment, project: project))
- end
-
before do
stub_pages_setting(host: 'example.com')
end
@@ -24,10 +20,6 @@ RSpec.describe Gitlab::Pages::VirtualHostFinder, feature_category: :pages do
subject(:virtual_domain) { described_class.new(pages_domain.domain).execute }
context 'when there are no pages deployed for the project' do
- before_all do
- project.mark_pages_as_not_deployed
- end
-
it 'returns nil' do
expect(virtual_domain).to be_nil
end
@@ -35,7 +27,7 @@ RSpec.describe Gitlab::Pages::VirtualHostFinder, feature_category: :pages do
context 'when there are pages deployed for the project' do
before_all do
- project.mark_pages_as_deployed
+ create(:pages_deployment, project: project)
end
it 'returns the virual domain when there are pages deployed for the project' do
@@ -48,10 +40,6 @@ RSpec.describe Gitlab::Pages::VirtualHostFinder, feature_category: :pages do
context 'when host is a namespace domain' do
context 'when there are no pages deployed for the project' do
- before_all do
- project.mark_pages_as_not_deployed
- end
-
it 'returns no result if the provided host is not subdomain of the Pages host' do
virtual_domain = described_class.new("#{project.namespace.path}.something.io").execute
@@ -68,7 +56,7 @@ RSpec.describe Gitlab::Pages::VirtualHostFinder, feature_category: :pages do
context 'when there are pages deployed for the project' do
before_all do
- project.mark_pages_as_deployed
+ create(:pages_deployment, project: project)
project.namespace.update!(path: 'topNAMEspace')
end
@@ -109,10 +97,6 @@ RSpec.describe Gitlab::Pages::VirtualHostFinder, feature_category: :pages do
end
context 'when there are no pages deployed for the project' do
- before_all do
- project.mark_pages_as_not_deployed
- end
-
it 'returns nil' do
expect(virtual_domain).to be_nil
end
@@ -120,7 +104,7 @@ RSpec.describe Gitlab::Pages::VirtualHostFinder, feature_category: :pages do
context 'when there are pages deployed for the project' do
before_all do
- project.mark_pages_as_deployed
+ create(:pages_deployment, project: project)
end
it 'returns the virual domain when there are pages deployed for the project' do
@@ -133,9 +117,10 @@ RSpec.describe Gitlab::Pages::VirtualHostFinder, feature_category: :pages do
it 'prioritizes the unique domain project' do
group = create(:group, path: 'unique-domain')
other_project = build(:project, path: 'unique-domain.example.com', group: group)
- other_project.save!(validate: false)
- other_project.update_pages_deployment!(create(:pages_deployment, project: other_project))
- other_project.mark_pages_as_deployed
+ .tap { |project| project.save!(validate: false) }
+
+ create(:pages_deployment, project: project)
+ create(:pages_deployment, project: other_project)
expect(virtual_domain).to be_an_instance_of(Pages::VirtualDomain)
expect(virtual_domain.lookup_paths.first.project_id).to eq(project.id)
@@ -150,10 +135,6 @@ RSpec.describe Gitlab::Pages::VirtualHostFinder, feature_category: :pages do
end
context 'when there are no pages deployed for the project' do
- before_all do
- project.mark_pages_as_not_deployed
- end
-
it 'returns nil' do
expect(virtual_domain).to be_nil
end
@@ -161,7 +142,7 @@ RSpec.describe Gitlab::Pages::VirtualHostFinder, feature_category: :pages do
context 'when there are pages deployed for the project' do
before_all do
- project.mark_pages_as_deployed
+ create(:pages_deployment, project: project)
end
it 'returns nil' do
diff --git a/spec/lib/gitlab/pagination/cursor_based_keyset_spec.rb b/spec/lib/gitlab/pagination/cursor_based_keyset_spec.rb
index effe767e41d..e5958549a81 100644
--- a/spec/lib/gitlab/pagination/cursor_based_keyset_spec.rb
+++ b/spec/lib/gitlab/pagination/cursor_based_keyset_spec.rb
@@ -28,7 +28,9 @@ RSpec.describe Gitlab::Pagination::CursorBasedKeyset do
end
describe '.enforced_for_type?' do
- subject { described_class.enforced_for_type?(relation) }
+ let_it_be(:project) { create(:project) }
+
+ subject { described_class.enforced_for_type?(project, relation) }
context 'when relation is Group' do
let(:relation) { Group.all }
@@ -45,7 +47,21 @@ RSpec.describe Gitlab::Pagination::CursorBasedKeyset do
context 'when relation is Ci::Build' do
let(:relation) { Ci::Build.all }
- it { is_expected.to be false }
+ before do
+ stub_feature_flags(enforce_ci_builds_pagination_limit: false)
+ end
+
+ context 'when feature flag enforce_ci_builds_pagination_limit is enabled' do
+ before do
+ stub_feature_flags(enforce_ci_builds_pagination_limit: project)
+ end
+
+ it { is_expected.to be true }
+ end
+
+ context 'when feature fllag enforce_ci_builds_pagination_limit is disabled' do
+ it { is_expected.to be false }
+ end
end
end
diff --git a/spec/lib/gitlab/pagination/keyset/order_spec.rb b/spec/lib/gitlab/pagination/keyset/order_spec.rb
index 05bb0bb8b3a..52d2688c06e 100644
--- a/spec/lib/gitlab/pagination/keyset/order_spec.rb
+++ b/spec/lib/gitlab/pagination/keyset/order_spec.rb
@@ -726,7 +726,7 @@ RSpec.describe Gitlab::Pagination::Keyset::Order do
end
describe '#attribute_names' do
- let(:expected_attribute_names) { %w(id name) }
+ let(:expected_attribute_names) { %w[id name] }
let(:order) do
described_class.build(
[
diff --git a/spec/lib/gitlab/pagination/offset_header_builder_spec.rb b/spec/lib/gitlab/pagination/offset_header_builder_spec.rb
index a415bad5135..f43216702a8 100644
--- a/spec/lib/gitlab/pagination/offset_header_builder_spec.rb
+++ b/spec/lib/gitlab/pagination/offset_header_builder_spec.rb
@@ -15,11 +15,15 @@ RSpec.describe Gitlab::Pagination::OffsetHeaderBuilder do
describe '#execute' do
let(:basic_links) do
- %{<http://localhost?page=1&per_page=5>; rel="prev", <http://localhost?page=3&per_page=5>; rel="next", <http://localhost?page=1&per_page=5>; rel="first"}
+ [
+ %(<http://localhost?page=1&per_page=5>; rel="prev"),
+ %(<http://localhost?page=3&per_page=5>; rel="next"),
+ %(<http://localhost?page=1&per_page=5>; rel="first")
+ ].join(', ')
end
let(:last_link) do
- %{, <http://localhost?page=3&per_page=5>; rel="last"}
+ %(, <http://localhost?page=3&per_page=5>; rel="last")
end
def expect_basic_headers
diff --git a/spec/lib/gitlab/patch/sidekiq_scheduled_enq_spec.rb b/spec/lib/gitlab/patch/sidekiq_scheduled_enq_spec.rb
index f57257cd1c0..cd3718f5dcc 100644
--- a/spec/lib/gitlab/patch/sidekiq_scheduled_enq_spec.rb
+++ b/spec/lib/gitlab/patch/sidekiq_scheduled_enq_spec.rb
@@ -10,7 +10,7 @@ RSpec.describe Gitlab::Patch::SidekiqScheduledEnq, :clean_gitlab_redis_queues, f
allow(Sidekiq).to receive(:load_json).and_return(payload)
# stub data in both namespaces
- Sidekiq.redis { |c| c.zadd('schedule', 100, 'dummy') }
+ Gitlab::Redis::Queues.with { |c| c.zadd('resque:gitlab:schedule', 100, 'dummy') }
Gitlab::Redis::Queues.with { |c| c.zadd('schedule', 100, 'dummy') }
end
@@ -26,7 +26,7 @@ RSpec.describe Gitlab::Patch::SidekiqScheduledEnq, :clean_gitlab_redis_queues, f
end
Gitlab::Redis::Queues.with do |conn|
- expect(conn.zcard('schedule')).to eq(0)
+ expect(conn.zcard('resque:gitlab:schedule')).to eq(0)
end
end
@@ -45,29 +45,13 @@ RSpec.describe Gitlab::Patch::SidekiqScheduledEnq, :clean_gitlab_redis_queues, f
end
Gitlab::Redis::Queues.with do |conn|
- expect(conn.zcard('schedule')).to eq(1)
+ expect(conn.zcard('resque:gitlab:schedule')).to eq(1)
end
end
end
- context 'when both envvar are enabled' do
- around do |example|
- # runs the zadd to ensure it goes into namespaced set
- Sidekiq.redis { |c| c.zadd('schedule', 100, 'dummy') }
-
- holder = Sidekiq.redis_pool
-
- # forcibly replace Sidekiq.redis since this is set in config/initializer/sidekiq.rb
- Sidekiq.redis = Gitlab::Redis::Queues.pool
-
- example.run
-
- ensure
- Sidekiq.redis = holder
- end
-
+ context 'when SIDEKIQ_ENABLE_DUAL_NAMESPACE_POLLING is enabled' do
before do
- stub_env('SIDEKIQ_ENQUEUE_NON_NAMESPACED', 'true')
stub_env('SIDEKIQ_ENABLE_DUAL_NAMESPACE_POLLING', 'true')
end
@@ -81,7 +65,7 @@ RSpec.describe Gitlab::Patch::SidekiqScheduledEnq, :clean_gitlab_redis_queues, f
end
Gitlab::Redis::Queues.with do |conn|
- expect(conn.zcard('schedule')).to eq(0)
+ expect(conn.zcard('resque:gitlab:schedule')).to eq(0)
end
end
end
diff --git a/spec/lib/gitlab/path_regex_spec.rb b/spec/lib/gitlab/path_regex_spec.rb
index 53dc145dcc4..eee05c0bb15 100644
--- a/spec/lib/gitlab/path_regex_spec.rb
+++ b/spec/lib/gitlab/path_regex_spec.rb
@@ -103,21 +103,27 @@ RSpec.describe Gitlab::PathRegex do
.concat(Array(API::API.prefix.to_s))
.concat(sitemap_words)
.concat(deprecated_routes)
+ .concat(reserved_routes)
.compact
.uniq
end
let(:sitemap_words) do
- %w(sitemap sitemap.xml sitemap.xml.gz)
+ %w[sitemap sitemap.xml sitemap.xml.gz]
end
let(:deprecated_routes) do
# profile was deprecated in https://gitlab.com/gitlab-org/gitlab/-/merge_requests/51646
- %w(profile s)
+ %w[profile s]
+ end
+
+ let(:reserved_routes) do
+ # login was reserved in https://gitlab.com/gitlab-org/gitlab/-/merge_requests/134791
+ ['login']
end
let(:ee_top_level_words) do
- %w(unsubscribes v2)
+ %w[unsubscribes v2]
end
let(:files_in_public) do
@@ -126,7 +132,7 @@ RSpec.describe Gitlab::PathRegex do
.split("\n")
.map { |entry| entry.start_with?('public/-/') ? '-' : entry.gsub('public/', '') }
.uniq
- tracked + %w(assets uploads)
+ tracked + %w[assets uploads]
end
# All routes that start with a namespaced path, that have 1 or more
diff --git a/spec/lib/gitlab/popen_spec.rb b/spec/lib/gitlab/popen_spec.rb
index 0a186b07d19..78455cb705f 100644
--- a/spec/lib/gitlab/popen_spec.rb
+++ b/spec/lib/gitlab/popen_spec.rb
@@ -24,7 +24,7 @@ RSpec.describe Gitlab::Popen do
context 'zero status' do
before do
- @output, @status = @klass.new.popen(%w(ls), path)
+ @output, @status = @klass.new.popen(%w[ls], path)
end
it { expect(@status).to be_zero }
@@ -33,7 +33,7 @@ RSpec.describe Gitlab::Popen do
context 'non-zero status' do
before do
- @output, @status = @klass.new.popen(%w(cat NOTHING), path)
+ @output, @status = @klass.new.popen(%w[cat NOTHING], path)
end
it { expect(@status).to eq(1) }
@@ -64,7 +64,7 @@ RSpec.describe Gitlab::Popen do
it 'calls popen3 with the provided environment variables' do
expect(Open3).to receive(:popen3).with(vars, 'ls', options)
- @output, @status = @klass.new.popen(%w(ls), path, { 'foobar' => 123 })
+ @output, @status = @klass.new.popen(%w[ls], path, { 'foobar' => 123 })
end
end
@@ -83,7 +83,7 @@ RSpec.describe Gitlab::Popen do
context 'without a directory argument' do
before do
- @output, @status = @klass.new.popen(%w(ls))
+ @output, @status = @klass.new.popen(%w[ls])
end
it { expect(@status).to be_zero }
diff --git a/spec/lib/gitlab/process_management_spec.rb b/spec/lib/gitlab/process_management_spec.rb
index fbd39702efb..709e4611954 100644
--- a/spec/lib/gitlab/process_management_spec.rb
+++ b/spec/lib/gitlab/process_management_spec.rb
@@ -8,7 +8,7 @@ RSpec.describe Gitlab::ProcessManagement do
expect(described_class).to receive(:trap).ordered.with(:INT)
expect(described_class).to receive(:trap).ordered.with(:HUP)
- described_class.trap_signals(%i(INT HUP))
+ described_class.trap_signals(%i[INT HUP])
end
end
@@ -17,7 +17,7 @@ RSpec.describe Gitlab::ProcessManagement do
expect(described_class).to receive(:trap).ordered.with(:INT, 'DEFAULT')
expect(described_class).to receive(:trap).ordered.with(:HUP, 'DEFAULT')
- described_class.modify_signals(%i(INT HUP), 'DEFAULT')
+ described_class.modify_signals(%i[INT HUP], 'DEFAULT')
end
end
diff --git a/spec/lib/gitlab/process_supervisor_spec.rb b/spec/lib/gitlab/process_supervisor_spec.rb
index 18de5053362..94535e96843 100644
--- a/spec/lib/gitlab/process_supervisor_spec.rb
+++ b/spec/lib/gitlab/process_supervisor_spec.rb
@@ -2,7 +2,7 @@
require_relative '../../../lib/gitlab/process_supervisor'
-RSpec.describe Gitlab::ProcessSupervisor, feature_category: :application_performance do
+RSpec.describe Gitlab::ProcessSupervisor, feature_category: :cloud_connector do
let(:health_check_interval_seconds) { 0.1 }
let(:check_terminate_interval_seconds) { 1 }
let(:forwarded_signals) { [] }
@@ -152,13 +152,13 @@ RSpec.describe Gitlab::ProcessSupervisor, feature_category: :application_perform
end
context 'termination signals' do
- let(:term_signals) { %i(INT TERM) }
+ let(:term_signals) { %i[INT TERM] }
context 'when TERM results in timely shutdown of processes' do
it 'forwards them to observed processes without waiting for grace period to expire' do
allow(Gitlab::ProcessManagement).to receive(:any_alive?).and_return(false)
- expect(Gitlab::ProcessManagement).to receive(:trap_signals).ordered.with(%i(INT TERM)).and_yield(:TERM)
+ expect(Gitlab::ProcessManagement).to receive(:trap_signals).ordered.with(%i[INT TERM]).and_yield(:TERM)
expect(Gitlab::ProcessManagement).to receive(:signal_processes).ordered.with(process_ids, :TERM)
expect(supervisor).not_to receive(:sleep).with(check_terminate_interval_seconds)
@@ -168,7 +168,7 @@ RSpec.describe Gitlab::ProcessSupervisor, feature_category: :application_perform
context 'when TERM does not result in timely shutdown of processes' do
it 'issues a KILL signal after the grace period expires' do
- expect(Gitlab::ProcessManagement).to receive(:trap_signals).with(%i(INT TERM)).and_yield(:TERM)
+ expect(Gitlab::ProcessManagement).to receive(:trap_signals).with(%i[INT TERM]).and_yield(:TERM)
expect(Gitlab::ProcessManagement).to receive(:signal_processes).ordered.with(process_ids, :TERM)
expect(supervisor).to receive(:sleep).ordered.with(check_terminate_interval_seconds).at_least(:once)
expect(Gitlab::ProcessManagement).to receive(:signal_processes).ordered.with(process_ids, '-KILL')
@@ -179,10 +179,10 @@ RSpec.describe Gitlab::ProcessSupervisor, feature_category: :application_perform
end
context 'forwarded signals' do
- let(:forwarded_signals) { %i(USR1) }
+ let(:forwarded_signals) { %i[USR1] }
it 'forwards given signals to the observed processes' do
- expect(Gitlab::ProcessManagement).to receive(:trap_signals).with(%i(USR1)).and_yield(:USR1)
+ expect(Gitlab::ProcessManagement).to receive(:trap_signals).with(%i[USR1]).and_yield(:USR1)
expect(Gitlab::ProcessManagement).to receive(:signal_processes).ordered.with(process_ids, :USR1)
supervisor.supervise(process_ids) { [] }
diff --git a/spec/lib/gitlab/project_template_spec.rb b/spec/lib/gitlab/project_template_spec.rb
index 998fff12e94..07cdbf97091 100644
--- a/spec/lib/gitlab/project_template_spec.rb
+++ b/spec/lib/gitlab/project_template_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::ProjectTemplate do
+RSpec.describe Gitlab::ProjectTemplate, feature_category: :source_code_management do
include ProjectTemplateTestHelper
describe '.all' do
diff --git a/spec/lib/gitlab/quick_actions/extractor_spec.rb b/spec/lib/gitlab/quick_actions/extractor_spec.rb
index f91e8d2a7ef..063b416c514 100644
--- a/spec/lib/gitlab/quick_actions/extractor_spec.rb
+++ b/spec/lib/gitlab/quick_actions/extractor_spec.rb
@@ -246,7 +246,7 @@ RSpec.describe Gitlab::QuickActions::Extractor, feature_category: :team_planning
msg = %(hello\nworld\n/reopen\n/shrug this is great?\n/shrug meh)
msg, commands = extractor.extract_commands(msg)
- expect(commands).to eq [['reopen'], ['shrug', 'this is great?'], %w(shrug meh)]
+ expect(commands).to eq [['reopen'], ['shrug', 'this is great?'], %w[shrug meh]]
expect(msg).to eq "hello\nworld\nthis is great? SHRUG\nmeh SHRUG"
end
diff --git a/spec/lib/gitlab/redis/multi_store_spec.rb b/spec/lib/gitlab/redis/multi_store_spec.rb
index 1745a745ec3..6b1c0fb2e81 100644
--- a/spec/lib/gitlab/redis/multi_store_spec.rb
+++ b/spec/lib/gitlab/redis/multi_store_spec.rb
@@ -3,7 +3,6 @@
require 'spec_helper'
RSpec.describe Gitlab::Redis::MultiStore, feature_category: :redis do
- using RSpec::Parameterized::TableSyntax
include RedisHelpers
let_it_be(:redis_store_class) { define_helper_redis_store_class }
@@ -56,7 +55,6 @@ RSpec.describe Gitlab::Redis::MultiStore, feature_category: :redis do
context 'when primary_store is not a ::Redis instance' do
before do
allow(primary_store).to receive(:is_a?).with(::Redis).and_return(false)
- allow(primary_store).to receive(:is_a?).with(::Redis::Namespace).and_return(false)
end
it 'fails with exception' do
@@ -65,21 +63,9 @@ RSpec.describe Gitlab::Redis::MultiStore, feature_category: :redis do
end
end
- context 'when primary_store is a ::Redis::Namespace instance' do
- before do
- allow(primary_store).to receive(:is_a?).with(::Redis).and_return(false)
- allow(primary_store).to receive(:is_a?).with(::Redis::Namespace).and_return(true)
- end
-
- it 'fails with exception' do
- expect { described_class.new(primary_store, secondary_store, instance_name) }.not_to raise_error
- 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)
- allow(secondary_store).to receive(:is_a?).with(::Redis::Namespace).and_return(false)
end
it 'fails with exception' do
@@ -88,130 +74,22 @@ RSpec.describe Gitlab::Redis::MultiStore, feature_category: :redis do
end
end
- context 'when secondary_store is a ::Redis::Namespace instance' do
- before do
- allow(secondary_store).to receive(:is_a?).with(::Redis).and_return(false)
- allow(secondary_store).to receive(:is_a?).with(::Redis::Namespace).and_return(true)
- end
-
- it 'fails with exception' do
- expect { described_class.new(primary_store, secondary_store, instance_name) }.not_to raise_error
- end
- end
-
# rubocop:disable RSpec/MultipleMemoizedHelpers
context 'with READ redis commands' do
subject do
multi_store.send(name, *args, **kwargs)
end
- 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(:skey2) { "redis:set:key2" }
- let_it_be(:smemberargs) { [skey, value1] }
- let_it_be(:hkey) { "redis:hash:key" }
- let_it_be(:hkey2) { "redis:hash:key2" }
- let_it_be(:zkey) { "redis:sortedset:key" }
- let_it_be(:zkey2) { "redis:sortedset:key2" }
- let_it_be(:hitem1) { "item1" }
- let_it_be(:hitem2) { "item2" }
- let_it_be(:keys) { [key1, key2] }
- let_it_be(:values) { [value1, value2] }
- let_it_be(:svalues) { [value2, value1] }
- let_it_be(:hgetargs) { [hkey, hitem1] }
- let_it_be(:hmgetval) { [value1] }
- let_it_be(:mhmgetargs) { [hkey, hitem1] }
- let_it_be(:hvalmapped) { { "item1" => value1 } }
- let_it_be(:sscanargs) { [skey2, 0] }
- let_it_be(:sscanval) { ["0", [value1]] }
- let_it_be(:scanargs) { ["0"] }
- let_it_be(:scankwargs) { { match: '*:set:key2*' } }
- let_it_be(:scanval) { ["0", [skey2]] }
- let_it_be(:sscan_eachval) { [value1] }
- let_it_be(:sscan_each_arg) { { match: '*1*' } }
- let_it_be(:hscan_eachval) { [[hitem1, value1]] }
- let_it_be(:zscan_eachval) { [[value1, 1.0]] }
- let_it_be(:scan_each_arg) { { match: 'redis*' } }
- let_it_be(:scan_each_val) { [key1, key2, skey, skey2, hkey, hkey2, zkey, zkey2] }
-
- # rubocop:disable Layout/LineLength
- where(:case_name, :name, :args, :value, :kwargs, :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
- 'execute :sismember command' | :sismember | ref(:smemberargs) | true | {} | nil
- 'execute :exists command' | :exists | ref(:key1) | 1 | {} | nil
- 'execute :exists? command' | :exists? | ref(:key1) | true | {} | nil
- 'execute :hget command' | :hget | ref(:hgetargs) | ref(:value1) | {} | nil
- 'execute :hlen command' | :hlen | ref(:hkey) | 1 | {} | nil
- 'execute :hgetall command' | :hgetall | ref(:hkey) | ref(:hvalmapped) | {} | nil
- 'execute :hexists command' | :hexists | ref(:hgetargs) | true | {} | nil
- 'execute :hmget command' | :hmget | ref(:hgetargs) | ref(:hmgetval) | {} | nil
- 'execute :mapped_hmget command' | :mapped_hmget | ref(:mhmgetargs) | ref(:hvalmapped) | {} | nil
- 'execute :sscan command' | :sscan | ref(:sscanargs) | ref(:sscanval) | {} | nil
- 'execute :scan command' | :scan | ref(:scanargs) | ref(:scanval) | ref(:scankwargs) | nil
-
- # we run *scan_each here as they are reads too
- 'execute :scan_each command' | :scan_each | nil | ref(:scan_each_val) | ref(:scan_each_arg) | nil
- 'execute :sscan_each command' | :sscan_each | ref(:skey2) | ref(:sscan_eachval) | {} | nil
- 'execute :sscan_each w block' | :sscan_each | ref(:skey) | ref(:sscan_eachval) | ref(:sscan_each_arg) | nil
- 'execute :hscan_each command' | :hscan_each | ref(:hkey) | ref(:hscan_eachval) | {} | nil
- 'execute :hscan_each w block' | :hscan_each | ref(:hkey2) | ref(:hscan_eachval) | ref(:sscan_each_arg) | nil
- 'execute :zscan_each command' | :zscan_each | ref(:zkey) | ref(:zscan_eachval) | {} | nil
- 'execute :zscan_each w block' | :zscan_each | ref(:zkey2) | ref(:zscan_eachval) | ref(:sscan_each_arg) | nil
- end
- # rubocop:enable Layout/LineLength
-
- before do
- primary_store.set(key1, value1)
- primary_store.set(key2, value2)
- primary_store.sadd?(skey, [value1, value2])
- primary_store.sadd?(skey2, [value1])
- primary_store.hset(hkey, hitem1, value1)
- primary_store.hset(hkey2, hitem1, value1, hitem2, value2)
- primary_store.zadd(zkey, 1, value1)
- primary_store.zadd(zkey2, [[1, value1], [2, value2]])
-
- secondary_store.set(key1, value1)
- secondary_store.set(key2, value2)
- secondary_store.sadd?(skey, [value1, value2])
- secondary_store.sadd?(skey2, [value1])
- secondary_store.hset(hkey, hitem1, value1)
- secondary_store.hset(hkey2, hitem1, value1, hitem2, value2)
- secondary_store.zadd(zkey, 1, value1)
- secondary_store.zadd(zkey2, [[1, value1], [2, value2]])
- end
-
- after do
- primary_store.flushdb
- secondary_store.flushdb
- 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
+ let(:args) { 'args' }
+ let(:kwargs) { { match: '*:set:key2*' } }
RSpec.shared_examples_for 'secondary store' do
it 'execute on the secondary instance' do
- expect(secondary_store).to receive(name).with(*expected_args).and_call_original
+ expect(secondary_store).to receive(name).with(*expected_args)
subject
end
- include_examples 'reads correct value'
-
it 'does not execute on the primary store' do
expect(primary_store).not_to receive(name)
@@ -219,23 +97,22 @@ RSpec.describe Gitlab::Redis::MultiStore, feature_category: :redis do
end
end
- with_them do
+ described_class::READ_COMMANDS.each do |name|
describe name.to_s do
- let(:expected_args) { kwargs&.present? ? [*args, { **kwargs }] : Array(args) }
+ let(:expected_args) { [*args, { **kwargs }] }
+ let(:name) { name }
before do
- allow(primary_store).to receive(name).and_call_original
- allow(secondary_store).to receive(name).and_call_original
+ allow(primary_store).to receive(name)
+ allow(secondary_store).to receive(name)
end
context 'when reading from the primary is successful' do
it 'returns the correct value' do
- expect(primary_store).to receive(name).with(*expected_args).and_call_original
+ expect(primary_store).to receive(name).with(*expected_args)
subject
end
-
- include_examples 'reads correct value'
end
context 'when reading from default instance is raising an exception' do
@@ -246,23 +123,13 @@ RSpec.describe Gitlab::Redis::MultiStore, feature_category: :redis do
it 'logs the exception and re-raises the error' do
expect(Gitlab::ErrorTracking).to receive(:log_exception).with(an_instance_of(StandardError),
- hash_including(:multi_store_error_message, instance_name: instance_name, command_name: name))
+ hash_including(:multi_store_error_message,
+ instance_name: instance_name, command_name: name))
expect { subject }.to raise_error(an_instance_of(StandardError))
end
end
- context 'when reading from empty default instance' do
- before do
- # this ensures a cache miss without having to stub the default store
- multi_store.default_store.flushdb
- end
-
- it 'does not call the non_default_store' do
- expect(multi_store.non_default_store).not_to receive(name)
- end
- end
-
context 'when the command is executed within pipelined block' do
subject do
multi_store.pipelined do |pipeline|
@@ -276,7 +143,7 @@ RSpec.describe Gitlab::Redis::MultiStore, feature_category: :redis do
2.times do
expect_next_instance_of(Redis::PipelinedConnection) do |pipeline|
- expect(pipeline).to receive(name).with(*expected_args).once.and_call_original
+ expect(pipeline).to receive(name).with(*expected_args).once
end
end
@@ -284,27 +151,16 @@ RSpec.describe Gitlab::Redis::MultiStore, feature_category: :redis do
end
end
- if params[:block]
+ context 'when block provided' do
subject do
- multi_store.send(name, *expected_args, &block)
+ multi_store.send(name, expected_args) { nil }
end
- context 'when block is provided' do
- it 'only default store yields to the block' do
- expect(primary_store).to receive(name).and_yield(value)
- expect(secondary_store).not_to receive(name).and_yield(value)
-
- subject
- end
-
- it 'only default store to execute' do
- expect(primary_store).to receive(name).with(*expected_args).and_call_original
- expect(secondary_store).not_to receive(name).with(*expected_args).and_call_original
-
- subject
- end
+ it 'only default store to execute' do
+ expect(primary_store).to receive(:send).with(name, expected_args)
+ expect(secondary_store).not_to receive(:send)
- include_examples 'reads correct value'
+ subject
end
end
@@ -327,8 +183,8 @@ RSpec.describe Gitlab::Redis::MultiStore, feature_category: :redis do
end
it 'executes only on secondary redis store', :aggregate_failures do
- expect(secondary_store).to receive(name).with(*expected_args).and_call_original
- expect(primary_store).not_to receive(name).with(*expected_args).and_call_original
+ expect(secondary_store).to receive(name).with(*expected_args)
+ expect(primary_store).not_to receive(name).with(*expected_args)
subject
end
@@ -336,8 +192,8 @@ RSpec.describe Gitlab::Redis::MultiStore, feature_category: :redis do
context 'when using primary store as default' do
it 'executes only on primary redis store', :aggregate_failures do
- expect(primary_store).to receive(name).with(*expected_args).and_call_original
- expect(secondary_store).not_to receive(name).with(*expected_args).and_call_original
+ expect(primary_store).to receive(name).with(*expected_args)
+ expect(secondary_store).not_to receive(name).with(*expected_args)
subject
end
@@ -429,110 +285,24 @@ RSpec.describe Gitlab::Redis::MultiStore, feature_category: :redis do
end
end
- RSpec.shared_examples_for 'verify that store contains values' do |store|
- it "#{store} redis store contains correct values", :aggregate_failures 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
-
- # rubocop:disable RSpec/MultipleMemoizedHelpers
context 'with WRITE redis commands' do
- let_it_be(:ikey1) { "counter1" }
- let_it_be(:ikey2) { "counter2" }
- let_it_be(:iargs) { [ikey2, 3] }
- let_it_be(:ivalue1) { "1" }
- let_it_be(:ivalue2) { "3" }
- let_it_be(:key1) { "redis:{1}:key_a" }
- let_it_be(:key2) { "redis:{1}:key_b" }
- let_it_be(:key3) { "redis:{1}:key_c" }
- let_it_be(:key4) { "redis:{1}:key_d" }
- 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]] }
- let_it_be(:script) { %(redis.call("set", "#{key1}", "#{value1}")) }
- let_it_be(:hkey1) { "redis:{1}:hash_a" }
- let_it_be(:hkey2) { "redis:{1}:hash_b" }
- let_it_be(:item) { "item" }
- let_it_be(:hdelarg) { [hkey1, item] }
- let_it_be(:hsetarg) { [hkey2, item, value1] }
- let_it_be(:mhsetarg) { [hkey2, { "item" => value1 }] }
- let_it_be(:hgetarg) { [hkey2, item] }
- let_it_be(:expireargs) { [key3, ttl] }
-
- # rubocop:disable Layout/LineLength
- 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 :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 :unlink command' | :unlink | ref(:key3) | nil | :get | ref(:key3)
- 'execute :flushdb command' | :flushdb | nil | 0 | :dbsize | nil
- 'execute :eval command' | :eval | ref(:script) | ref(:value1) | :get | ref(:key1)
- 'execute :incr command' | :incr | ref(:ikey1) | ref(:ivalue1) | :get | ref(:ikey1)
- 'execute :incrby command' | :incrby | ref(:iargs) | ref(:ivalue2) | :get | ref(:ikey2)
- 'execute :hset command' | :hset | ref(:hsetarg) | ref(:value1) | :hget | ref(:hgetarg)
- 'execute :hdel command' | :hdel | ref(:hdelarg) | nil | :hget | ref(:hdelarg)
- 'execute :expire command' | :expire | ref(:expireargs) | ref(:ttl) | :ttl | ref(:key3)
- 'execute :mapped_hmset command' | :mapped_hmset | ref(:mhsetarg) | ref(:value1) | :hget | ref(:hgetarg)
- end
- # rubocop:enable Layout/LineLength
-
- before do
- primary_store.flushdb
- secondary_store.flushdb
-
- primary_store.set(key2, value1)
- primary_store.set(key3, value1)
- primary_store.set(key4, value1)
- primary_store.sadd?(skey, value1)
- primary_store.hset(hkey2, item, value1)
-
- secondary_store.set(key2, value1)
- secondary_store.set(key3, value1)
- secondary_store.set(key4, value1)
- secondary_store.sadd?(skey, value1)
- secondary_store.hset(hkey2, item, value1)
- end
-
- with_them do
+ described_class::WRITE_COMMANDS.each do |name|
describe name.to_s do
- let(:expected_args) { args || no_args }
+ let(:args) { "dummy_args" }
+ let(:name) { name }
before do
- allow(primary_store).to receive(name).and_call_original
- allow(secondary_store).to receive(name).and_call_original
+ allow(primary_store).to receive(name)
+ allow(secondary_store).to receive(name)
end
context 'when executing on primary instance is successful' do
it 'executes on both primary and secondary redis store', :aggregate_failures do
- expect(primary_store).to receive(name).with(*expected_args).and_call_original
- expect(secondary_store).to receive(name).with(*expected_args).and_call_original
+ expect(primary_store).to receive(name).with(*args)
+ expect(secondary_store).to receive(name).with(*args)
subject
end
-
- include_examples 'verify that store contains values', :primary_store
- include_examples 'verify that store contains values', :secondary_store
end
context 'when use_primary_and_secondary_stores feature flag is disabled' do
@@ -546,8 +316,8 @@ RSpec.describe Gitlab::Redis::MultiStore, feature_category: :redis do
end
it 'executes only on secondary redis store', :aggregate_failures do
- expect(secondary_store).to receive(name).with(*expected_args).and_call_original
- expect(primary_store).not_to receive(name).with(*expected_args).and_call_original
+ expect(secondary_store).to receive(name).with(*args)
+ expect(primary_store).not_to receive(name).with(*args)
subject
end
@@ -555,8 +325,8 @@ RSpec.describe Gitlab::Redis::MultiStore, feature_category: :redis do
context 'when using primary store as default' do
it 'executes only on primary redis store', :aggregate_failures do
- expect(primary_store).to receive(name).with(*expected_args).and_call_original
- expect(secondary_store).not_to receive(name).with(*expected_args).and_call_original
+ expect(primary_store).to receive(name).with(*args)
+ expect(secondary_store).not_to receive(name).with(*args)
subject
end
@@ -565,31 +335,30 @@ RSpec.describe Gitlab::Redis::MultiStore, feature_category: :redis do
context 'when executing on the default instance is raising an exception' do
before do
- allow(multi_store.default_store).to receive(name).with(*expected_args).and_raise(StandardError)
+ allow(multi_store.default_store).to receive(name).with(*args).and_raise(StandardError)
allow(Gitlab::ErrorTracking).to receive(:log_exception)
end
it 'raises error and does not execute on non default instance', :aggregate_failures do
- expect(multi_store.non_default_store).not_to receive(name).with(*expected_args)
+ expect(multi_store.non_default_store).not_to receive(name).with(*args)
expect { subject }.to raise_error(StandardError)
end
end
context 'when executing on the non default instance is raising an exception' do
before do
- allow(multi_store.non_default_store).to receive(name).with(*expected_args).and_raise(StandardError)
+ allow(multi_store.non_default_store).to receive(name).with(*args).and_raise(StandardError)
allow(Gitlab::ErrorTracking).to receive(:log_exception)
end
it 'logs the exception and execute on default instance', :aggregate_failures do
expect(Gitlab::ErrorTracking).to receive(:log_exception).with(an_instance_of(StandardError),
- hash_including(:multi_store_error_message, command_name: name, instance_name: instance_name))
- expect(multi_store.default_store).to receive(name).with(*expected_args).and_call_original
+ hash_including(:multi_store_error_message,
+ command_name: name, instance_name: instance_name))
+ expect(multi_store.default_store).to receive(name).with(*args)
subject
end
-
- include_examples 'verify that store contains values', :default_store
end
context 'when the command is executed within pipelined block' do
@@ -602,24 +371,35 @@ RSpec.describe Gitlab::Redis::MultiStore, feature_category: :redis do
it 'is executed only 1 time on each instance', :aggregate_failures do
expect(primary_store).to receive(:pipelined).and_call_original
expect_next_instance_of(Redis::PipelinedConnection) do |pipeline|
- expect(pipeline).to receive(name).with(*expected_args).once.and_call_original
+ expect(pipeline).to receive(name).with(*args).once
end
expect(secondary_store).to receive(:pipelined).and_call_original
expect_next_instance_of(Redis::PipelinedConnection) do |pipeline|
- expect(pipeline).to receive(name).with(*expected_args).once.and_call_original
+ expect(pipeline).to receive(name).with(*args).once
end
subject
end
-
- include_examples 'verify that store contains values', :primary_store
- include_examples 'verify that store contains values', :secondary_store
end
end
end
end
- # rubocop:enable RSpec/MultipleMemoizedHelpers
+
+ RSpec.shared_examples_for 'verify that store contains values' do |store|
+ it "#{store} redis store contains correct values", :aggregate_failures 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
RSpec.shared_examples_for 'pipelined command' do |name|
let_it_be(:key1) { "redis:{1}:key_a" }
@@ -951,11 +731,7 @@ RSpec.describe Gitlab::Redis::MultiStore, feature_category: :redis do
describe '#close' do
subject { multi_store.close }
- context 'when using both stores' do
- before do
- allow(multi_store).to receive(:use_primary_and_secondary_stores?).and_return(true)
- end
-
+ context 'when using both stores are different' do
it 'closes both stores' do
expect(primary_store).to receive(:close)
expect(secondary_store).to receive(:close)
@@ -964,36 +740,72 @@ RSpec.describe Gitlab::Redis::MultiStore, feature_category: :redis do
end
end
- context 'when using only one store' do
+ context 'when using identical stores' do
before do
- allow(multi_store).to receive(:use_primary_and_secondary_stores?).and_return(false)
+ allow(multi_store).to receive(:same_redis_store?).and_return(true)
end
- context 'when using primary_store as default store' do
- before do
- allow(multi_store).to receive(:use_primary_store_as_default?).and_return(true)
- end
+ it 'closes secondary store' do
+ expect(secondary_store).to receive(:close)
+ expect(primary_store).not_to receive(:close)
- it 'closes primary store' do
- expect(primary_store).to receive(:close)
- expect(secondary_store).not_to receive(:close)
+ subject
+ end
+ end
+ end
- subject
- end
+ describe '#blpop' do
+ let_it_be(:key) { "mylist" }
+
+ subject { multi_store.blpop(key, timeout: 0.1) }
+
+ shared_examples 'calls blpop on default_store' do
+ it 'calls blpop on default_store' do
+ expect(multi_store.default_store).to receive(:blpop).with(key, { timeout: 0.1 })
+
+ subject
end
+ end
- context 'when using secondary_store as default store' do
+ shared_examples 'does not call lpop on non_default_store' do
+ it 'does not call blpop on non_default_store' do
+ expect(multi_store.non_default_store).not_to receive(:blpop)
+
+ subject
+ end
+ end
+
+ context 'when using both stores' do
+ before do
+ allow(multi_store).to receive(:use_primary_and_secondary_stores?).and_return(true)
+ end
+
+ it_behaves_like 'calls blpop on default_store'
+
+ context "when an element exists in the default_store" do
before do
- allow(multi_store).to receive(:use_primary_store_as_default?).and_return(false)
+ multi_store.default_store.lpush(key, 'abc')
end
- it 'closes secondary store' do
- expect(primary_store).not_to receive(:close)
- expect(secondary_store).to receive(:close)
+ it 'calls lpop on non_default_store' do
+ expect(multi_store.non_default_store).to receive(:blpop).with(key, { timeout: 1 })
subject
end
end
+
+ context 'when the list is empty in default_store' do
+ it_behaves_like 'does not call lpop on non_default_store'
+ end
+ end
+
+ context 'when using one store' do
+ before do
+ allow(multi_store).to receive(:use_primary_and_secondary_stores?).and_return(false)
+ end
+
+ it_behaves_like 'calls blpop on default_store'
+ it_behaves_like 'does not call lpop on non_default_store'
end
end
@@ -1279,4 +1091,19 @@ RSpec.describe Gitlab::Redis::MultiStore, feature_category: :redis do
end
end
end
+
+ describe '*_COMMANDS' do
+ it 'checks if every command is only defined once' do
+ commands = [
+ described_class::REDIS_CLIENT_COMMANDS,
+ described_class::PUBSUB_SUBSCRIBE_COMMANDS,
+ described_class::READ_COMMANDS,
+ described_class::WRITE_COMMANDS,
+ described_class::PIPELINED_COMMANDS
+ ].inject([], :concat)
+ duplicated_commands = commands.group_by { |c| c }.select { |k, v| v.size > 1 }.map(&:first)
+
+ expect(duplicated_commands).to be_empty, "commands #{duplicated_commands} defined more than once"
+ end
+ end
end
diff --git a/spec/lib/gitlab/redis/pubsub_spec.rb b/spec/lib/gitlab/redis/pubsub_spec.rb
deleted file mode 100644
index e196d02116e..00000000000
--- a/spec/lib/gitlab/redis/pubsub_spec.rb
+++ /dev/null
@@ -1,8 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Gitlab::Redis::Pubsub, feature_category: :redis do
- include_examples "redis_new_instance_shared_examples", 'pubsub', Gitlab::Redis::SharedState
- include_examples "redis_shared_examples"
-end
diff --git a/spec/lib/gitlab/reference_extractor_spec.rb b/spec/lib/gitlab/reference_extractor_spec.rb
index 37db13b76b9..9b8143f7bb8 100644
--- a/spec/lib/gitlab/reference_extractor_spec.rb
+++ b/spec/lib/gitlab/reference_extractor_spec.rb
@@ -31,7 +31,7 @@ RSpec.describe Gitlab::ReferenceExtractor do
project.add_reporter(@u_foo)
project.add_reporter(@u_bar)
- subject.analyze(%{
+ subject.analyze(%(
Inline code: `@foo`
Code block:
@@ -43,7 +43,7 @@ RSpec.describe Gitlab::ReferenceExtractor do
Quote:
> @offteam
- })
+ ))
expect(subject.users).to match_array([])
end
diff --git a/spec/lib/gitlab/regex_requires_app_spec.rb b/spec/lib/gitlab/regex_requires_app_spec.rb
index bea5d25dbc8..7a247e5e8cf 100644
--- a/spec/lib/gitlab/regex_requires_app_spec.rb
+++ b/spec/lib/gitlab/regex_requires_app_spec.rb
@@ -5,6 +5,18 @@ require 'spec_helper'
# Only specs that *cannot* be run with fast_spec_helper only
# See regex_spec for tests that do not require the full spec_helper
RSpec.describe Gitlab::Regex, feature_category: :tooling do
+ shared_examples_for 'npm package name regex' do
+ it { is_expected.to match('@scope/package') }
+ it { is_expected.to match('unscoped-package') }
+ it { is_expected.not_to match('@first-scope@second-scope/package') }
+ it { is_expected.not_to match('scope-without-at-symbol/package') }
+ it { is_expected.not_to match('@not-a-scoped-package') }
+ it { is_expected.not_to match('@scope/sub/package') }
+ it { is_expected.not_to match('@scope/../../package') }
+ it { is_expected.not_to match('@scope%2e%2e%2fpackage') }
+ it { is_expected.not_to match('@%2e%2e%2f/package') }
+ end
+
describe '.debian_architecture_regex' do
subject { described_class.debian_architecture_regex }
@@ -37,15 +49,7 @@ RSpec.describe Gitlab::Regex, feature_category: :tooling do
describe '.npm_package_name_regex' do
subject { described_class.npm_package_name_regex }
- it { is_expected.to match('@scope/package') }
- it { is_expected.to match('unscoped-package') }
- it { is_expected.not_to match('@first-scope@second-scope/package') }
- it { is_expected.not_to match('scope-without-at-symbol/package') }
- it { is_expected.not_to match('@not-a-scoped-package') }
- it { is_expected.not_to match('@scope/sub/package') }
- it { is_expected.not_to match('@scope/../../package') }
- it { is_expected.not_to match('@scope%2e%2e%2fpackage') }
- it { is_expected.not_to match('@%2e%2e%2f/package') }
+ it_behaves_like 'npm package name regex'
context 'capturing group' do
[
@@ -63,6 +67,24 @@ RSpec.describe Gitlab::Regex, feature_category: :tooling do
end
end
+ describe '.protection_rules_npm_package_name_pattern_regex' do
+ subject { described_class.protection_rules_npm_package_name_pattern_regex }
+
+ it_behaves_like 'npm package name regex'
+
+ it { is_expected.to match('@scope/package-*') }
+ it { is_expected.to match('@my-scope/*my-package-with-wildcard-inbetween') }
+ it { is_expected.to match('@my-scope/*my-package-with-wildcard-start') }
+ it { is_expected.to match('@my-scope/my-*package-*with-wildcard-multiple-*') }
+ it { is_expected.to match('@my-scope/my-package-with_____underscore') }
+ it { is_expected.to match('@my-scope/my-package-with-wildcard-end*') }
+ it { is_expected.to match('@my-scope/my-package-with-regex-characters.+') }
+
+ it { is_expected.not_to match('@my-scope/my-package-with-percent-sign-%') }
+ it { is_expected.not_to match('*@my-scope/my-package-with-wildcard-start') }
+ it { is_expected.not_to match('@my-scope/my-package-with-backslash-\*') }
+ end
+
describe '.debian_distribution_regex' do
subject { described_class.debian_distribution_regex }
diff --git a/spec/lib/gitlab/repository_cache_adapter_spec.rb b/spec/lib/gitlab/repository_cache_adapter_spec.rb
index 71a20cc58fd..c35038b3b75 100644
--- a/spec/lib/gitlab/repository_cache_adapter_spec.rb
+++ b/spec/lib/gitlab/repository_cache_adapter_spec.rb
@@ -15,7 +15,7 @@ RSpec.describe Gitlab::RepositoryCacheAdapter do
include Gitlab::RepositoryCacheAdapter # can't use described_class here
def letters
- %w(b a c)
+ %w[b a c]
end
cache_method_as_redis_set(:letters)
@@ -47,11 +47,11 @@ RSpec.describe Gitlab::RepositoryCacheAdapter do
expect(fake_repository).to receive(:_uncached_letters).once.and_call_original
2.times do
- expect(fake_repository.letters).to eq(%w(a b c))
+ expect(fake_repository.letters).to eq(%w[a b c])
end
expect(redis_set_cache.exist?(:letters)).to eq(true)
- expect(fake_repository.instance_variable_get(:@letters)).to eq(%w(a b c))
+ expect(fake_repository.instance_variable_get(:@letters)).to eq(%w[a b c])
end
context 'membership checks' do
@@ -69,7 +69,7 @@ RSpec.describe Gitlab::RepositoryCacheAdapter do
context 'when the cache key exists' do
before do
- redis_set_cache.write(:letters, %w(b a c))
+ redis_set_cache.write(:letters, %w[b a c])
end
it 'calls #try_include? on the set cache' do
@@ -300,7 +300,7 @@ RSpec.describe Gitlab::RepositoryCacheAdapter do
expect(redis_set_cache).to receive(:expire).with(:branch_names)
expect(redis_hash_cache).to receive(:delete).with(:branch_names)
- repository.expire_method_caches(%i(branch_names))
+ repository.expire_method_caches(%i[branch_names])
end
it 'does not expire caches for non-existent methods' do
@@ -308,7 +308,7 @@ RSpec.describe Gitlab::RepositoryCacheAdapter do
expect(Gitlab::AppLogger).to(
receive(:error).with("Requested to expire non-existent method 'nonexistent' for Repository"))
- repository.expire_method_caches(%i(nonexistent))
+ repository.expire_method_caches(%i[nonexistent])
end
end
end
diff --git a/spec/lib/gitlab/repository_hash_cache_spec.rb b/spec/lib/gitlab/repository_hash_cache_spec.rb
index e3cc6ed69fb..4b4a2092c98 100644
--- a/spec/lib/gitlab/repository_hash_cache_spec.rb
+++ b/spec/lib/gitlab/repository_hash_cache_spec.rb
@@ -88,7 +88,7 @@ RSpec.describe Gitlab::RepositoryHashCache, :clean_gitlab_redis_repository_cache
describe "#read_members" do
subject { cache.read_members(:example, keys) }
- let(:keys) { %w(test missing) }
+ let(:keys) { %w[test missing] }
context "all data is cached" do
before do
@@ -140,7 +140,7 @@ RSpec.describe Gitlab::RepositoryHashCache, :clean_gitlab_redis_repository_cache
end
end
- let(:keys) { %w(test) }
+ let(:keys) { %w[test] }
it "records metrics" do
# Here we expect it to receive "test" as a missing key because we
@@ -151,7 +151,7 @@ RSpec.describe Gitlab::RepositoryHashCache, :clean_gitlab_redis_repository_cache
end
context "fully cached" do
- let(:keys) { %w(test another) }
+ let(:keys) { %w[test another] }
before do
cache.write(:example, test_hash.merge({ "another" => "not_missing" }))
@@ -169,7 +169,7 @@ RSpec.describe Gitlab::RepositoryHashCache, :clean_gitlab_redis_repository_cache
end
context "partially cached" do
- let(:keys) { %w(test missing) }
+ let(:keys) { %w[test missing] }
before do
cache.write(:example, test_hash)
@@ -187,7 +187,7 @@ RSpec.describe Gitlab::RepositoryHashCache, :clean_gitlab_redis_repository_cache
end
context "uncached" do
- let(:keys) { %w(test missing) }
+ let(:keys) { %w[test missing] }
it "returns a hash" do
is_expected.to eq({ "test" => "was_missing", "missing" => "was_missing" })
diff --git a/spec/lib/gitlab/repository_set_cache_spec.rb b/spec/lib/gitlab/repository_set_cache_spec.rb
index 23b2a2b9493..42378a16046 100644
--- a/spec/lib/gitlab/repository_set_cache_spec.rb
+++ b/spec/lib/gitlab/repository_set_cache_spec.rb
@@ -90,7 +90,7 @@ RSpec.describe Gitlab::RepositorySetCache, :clean_gitlab_redis_repository_cache,
end
context 'single key' do
- let(:keys) { %w(foo) }
+ let(:keys) { %w[foo] }
it { is_expected.to eq(1) }
@@ -102,7 +102,7 @@ RSpec.describe Gitlab::RepositorySetCache, :clean_gitlab_redis_repository_cache,
end
context 'multiple keys' do
- let(:keys) { %w(foo bar) }
+ let(:keys) { %w[foo bar] }
it { is_expected.to eq(2) }
diff --git a/spec/lib/gitlab/rugged_instrumentation_spec.rb b/spec/lib/gitlab/rugged_instrumentation_spec.rb
deleted file mode 100644
index 393bb957aba..00000000000
--- a/spec/lib/gitlab/rugged_instrumentation_spec.rb
+++ /dev/null
@@ -1,27 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Gitlab::RuggedInstrumentation, :request_store do
- subject { described_class }
-
- describe '.query_time' do
- it 'increments query times' do
- subject.add_query_time(0.4510004)
- subject.add_query_time(0.3220004)
-
- expect(subject.query_time).to eq(0.773001)
- expect(subject.query_time_ms).to eq(773.0)
- end
- end
-
- describe '.increment_query_count' do
- it 'tracks query counts' do
- expect(subject.query_count).to eq(0)
-
- 2.times { subject.increment_query_count }
-
- expect(subject.query_count).to eq(2)
- end
- end
-end
diff --git a/spec/lib/gitlab/runtime_spec.rb b/spec/lib/gitlab/runtime_spec.rb
index 01cfa91bb59..05bcdf2fc96 100644
--- a/spec/lib/gitlab/runtime_spec.rb
+++ b/spec/lib/gitlab/runtime_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::Runtime, feature_category: :application_performance do
+RSpec.describe Gitlab::Runtime, feature_category: :cloud_connector do
shared_examples "valid runtime" do |runtime, max_threads|
it "identifies itself" do
expect(subject.identify).to eq(runtime)
diff --git a/spec/lib/gitlab/search/abuse_detection_spec.rb b/spec/lib/gitlab/search/abuse_detection_spec.rb
index cbf20614ba5..d244234e763 100644
--- a/spec/lib/gitlab/search/abuse_detection_spec.rb
+++ b/spec/lib/gitlab/search/abuse_detection_spec.rb
@@ -20,7 +20,7 @@ RSpec.describe Gitlab::Search::AbuseDetection, feature_category: :global_search
end
describe 'abusive character matching' do
- refs = %w(
+ refs = %w[
main
тест
maiñ
@@ -30,7 +30,7 @@ RSpec.describe Gitlab::Search::AbuseDetection, feature_category: :global_search
feature/it_works
really_important!
测试
- )
+ ]
refs.each do |ref|
it "does match refs permitted by git refname: #{ref}" do
diff --git a/spec/lib/gitlab/search_results_spec.rb b/spec/lib/gitlab/search_results_spec.rb
index 00e68f73d2d..a3acb8b92ed 100644
--- a/spec/lib/gitlab/search_results_spec.rb
+++ b/spec/lib/gitlab/search_results_spec.rb
@@ -99,7 +99,7 @@ RSpec.describe Gitlab::SearchResults, feature_category: :global_search do
describe '#aggregations' do
where(:scope) do
- %w(projects issues merge_requests blobs commits wiki_blobs epics milestones users unknown)
+ %w[projects issues merge_requests blobs commits wiki_blobs epics milestones users unknown]
end
with_them do
@@ -197,7 +197,7 @@ RSpec.describe Gitlab::SearchResults, feature_category: :global_search do
let(:query) { 'foo' }
include_examples 'search results filtered by state'
- include_examples 'search results filtered by archived', 'search_merge_requests_hide_archived_projects'
+ include_examples 'search results filtered by archived'
end
context 'ordering' do
@@ -244,7 +244,7 @@ RSpec.describe Gitlab::SearchResults, feature_category: :global_search do
include_examples 'search results filtered by state'
include_examples 'search results filtered by confidential'
- include_examples 'search results filtered by archived', 'search_issues_hide_archived_projects'
+ include_examples 'search results filtered by archived'
end
context 'ordering' do
diff --git a/spec/lib/gitlab/security/scan_configuration_spec.rb b/spec/lib/gitlab/security/scan_configuration_spec.rb
index faa8a206d74..9151db3c5ff 100644
--- a/spec/lib/gitlab/security/scan_configuration_spec.rb
+++ b/spec/lib/gitlab/security/scan_configuration_spec.rb
@@ -15,7 +15,7 @@ RSpec.describe ::Gitlab::Security::ScanConfiguration do
let(:configured) { true }
context 'with a core scanner' do
- where(type: %i(sast sast_iac secret_detection container_scanning))
+ where(type: %i[sast sast_iac secret_detection container_scanning])
with_them do
it { is_expected.to be_truthy }
@@ -73,7 +73,7 @@ RSpec.describe ::Gitlab::Security::ScanConfiguration do
let(:configured) { true }
context 'with a core scanner' do
- where(type: %i(sast sast_iac secret_detection))
+ where(type: %i[sast sast_iac secret_detection])
with_them do
it { is_expected.to be_truthy }
diff --git a/spec/lib/gitlab/seeders/ci/catalog/resource_seeder_spec.rb b/spec/lib/gitlab/seeders/ci/catalog/resource_seeder_spec.rb
new file mode 100644
index 00000000000..4bd4455d1bd
--- /dev/null
+++ b/spec/lib/gitlab/seeders/ci/catalog/resource_seeder_spec.rb
@@ -0,0 +1,101 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe ::Gitlab::Seeders::Ci::Catalog::ResourceSeeder, feature_category: :pipeline_composition do
+ let_it_be(:admin) { create(:admin) }
+ let_it_be_with_reload(:group) { create(:group) }
+ let_it_be(:seed_count) { 2 }
+ let_it_be(:last_resource_id) { seed_count - 1 }
+
+ let(:group_path) { group.path }
+
+ subject(:seeder) { described_class.new(group_path: group_path, seed_count: seed_count) }
+
+ before_all do
+ group.add_owner(admin)
+ end
+
+ describe '#seed' do
+ subject(:seed) { seeder.seed }
+
+ context 'when the group does not exists' do
+ let(:group_path) { 'nonexistent_group' }
+
+ it 'skips seeding' do
+ expect { seed }.not_to change { Project.count }
+ end
+ end
+
+ context 'when project name already exists' do
+ before do
+ create(:project, namespace: group, name: "ci_seed_resource_0")
+ end
+
+ it 'skips that project creation and keeps seeding' do
+ expect { seed }.to change { Project.count }.by(seed_count - 1)
+ end
+ end
+
+ context 'when project.saved? fails' do
+ before do
+ project = build(:project, name: nil)
+
+ allow_next_instance_of(::Projects::CreateService) do |service|
+ allow(service).to receive(:execute).and_return(project)
+ end
+ end
+
+ it 'does not modify the projects count' do
+ expect { seed }.not_to change { Project.count }
+ end
+ end
+
+ context 'when ci resource creation fails' do
+ before do
+ allow_next_instance_of(::Ci::Catalog::Resources::CreateService) do |service|
+ allow(service).to receive(:execute).and_return(ServiceResponse.error(message: 'error'))
+ end
+ end
+
+ it 'does not add a catalog resource' do
+ expect { seed }.to change { Project.count }.by(seed_count)
+
+ expect(group.projects.all?(&:catalog_resource)).to eq false
+ end
+ end
+
+ it 'skips seeding a project if the project name already exists' do
+ # We call the same command twice, as it means it would try to recreate
+ # projects that were already created!
+ expect { seed }.to change { group.projects.count }.by(seed_count)
+ expect { seed }.to change { group.projects.count }.by(0)
+ end
+
+ it 'creates as many projects as specific in the argument' do
+ expect { seed }.to change {
+ group.projects.count
+ }.by(seed_count)
+
+ last_ci_resource = Project.last
+
+ expect(last_ci_resource.name).to eq "ci_seed_resource_#{last_resource_id}"
+ end
+
+ it 'adds a README and a template.yml file to the projects' do
+ seed
+ project = group.projects.last
+ default_branch = project.default_branch_or_main
+
+ expect(project.repository.blob_at(default_branch, "README.md")).not_to be_nil
+ expect(project.repository.blob_at(default_branch, "template.yml")).not_to be_nil
+ end
+
+ # This should be run again when fixing: https://gitlab.com/gitlab-org/gitlab/-/issues/429649
+ xit 'creates projects with CI catalog resources' do
+ expect { seed }.to change { Project.count }.by(seed_count)
+
+ expect(group.projects.all?(&:catalog_resource)).to eq true
+ end
+ end
+end
diff --git a/spec/lib/gitlab/shard_health_cache_spec.rb b/spec/lib/gitlab/shard_health_cache_spec.rb
index 0c25cc7dab5..8d0eebbb23e 100644
--- a/spec/lib/gitlab/shard_health_cache_spec.rb
+++ b/spec/lib/gitlab/shard_health_cache_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe Gitlab::ShardHealthCache, :clean_gitlab_redis_cache do
- let(:shards) { %w(foo bar) }
+ let(:shards) { %w[foo bar] }
before do
described_class.update(shards) # rubocop:disable Rails/SaveBang
@@ -23,7 +23,7 @@ RSpec.describe Gitlab::ShardHealthCache, :clean_gitlab_redis_cache do
end
it 'replaces the existing set' do
- new_set = %w(test me more)
+ new_set = %w[test me more]
described_class.update(new_set) # rubocop:disable Rails/SaveBang
expect(described_class.cached_healthy_shards).to match_array(new_set)
diff --git a/spec/lib/gitlab/sidekiq_config/cli_methods_spec.rb b/spec/lib/gitlab/sidekiq_config/cli_methods_spec.rb
index 576b36c1829..1145fd02891 100644
--- a/spec/lib/gitlab/sidekiq_config/cli_methods_spec.rb
+++ b/spec/lib/gitlab/sidekiq_config/cli_methods_spec.rb
@@ -173,7 +173,7 @@ RSpec.describe Gitlab::SidekiqConfig::CliMethods do
end
it 'returns the queue names of matched workers' do
- expect(described_class.query_queues(query, worker_metadatas)).to match(%w(a a:2 c))
+ expect(described_class.query_queues(query, worker_metadatas)).to match(%w[a a:2 c])
end
end
end
diff --git a/spec/lib/gitlab/sidekiq_config/worker_matcher_spec.rb b/spec/lib/gitlab/sidekiq_config/worker_matcher_spec.rb
index dfe9358f70b..08ead3282fd 100644
--- a/spec/lib/gitlab/sidekiq_config/worker_matcher_spec.rb
+++ b/spec/lib/gitlab/sidekiq_config/worker_matcher_spec.rb
@@ -51,61 +51,61 @@ RSpec.describe Gitlab::SidekiqConfig::WorkerMatcher do
context 'with valid input' do
where(:query, :expected_metadatas) do
# worker_name
- 'worker_name=WorkerA' | %w(WorkerA)
- 'worker_name=WorkerA2' | %w(WorkerA2)
- 'worker_name=WorkerB|worker_name=WorkerD' | %w(WorkerB)
- 'worker_name!=WorkerA' | %w(WorkerA2 WorkerB WorkerC)
+ 'worker_name=WorkerA' | %w[WorkerA]
+ 'worker_name=WorkerA2' | %w[WorkerA2]
+ 'worker_name=WorkerB|worker_name=WorkerD' | %w[WorkerB]
+ 'worker_name!=WorkerA' | %w[WorkerA2 WorkerB WorkerC]
# feature_category
- 'feature_category=category_a' | %w(WorkerA WorkerA2)
- 'feature_category=category_a,category_c' | %w(WorkerA WorkerA2 WorkerC)
- 'feature_category=category_a|feature_category=category_c' | %w(WorkerA WorkerA2 WorkerC)
- 'feature_category!=category_a' | %w(WorkerB WorkerC)
+ 'feature_category=category_a' | %w[WorkerA WorkerA2]
+ 'feature_category=category_a,category_c' | %w[WorkerA WorkerA2 WorkerC]
+ 'feature_category=category_a|feature_category=category_c' | %w[WorkerA WorkerA2 WorkerC]
+ 'feature_category!=category_a' | %w[WorkerB WorkerC]
# has_external_dependencies
- 'has_external_dependencies=true' | %w(WorkerB)
- 'has_external_dependencies=false' | %w(WorkerA WorkerA2 WorkerC)
- 'has_external_dependencies=true,false' | %w(WorkerA WorkerA2 WorkerB WorkerC)
- 'has_external_dependencies=true|has_external_dependencies=false' | %w(WorkerA WorkerA2 WorkerB WorkerC)
- 'has_external_dependencies!=true' | %w(WorkerA WorkerA2 WorkerC)
+ 'has_external_dependencies=true' | %w[WorkerB]
+ 'has_external_dependencies=false' | %w[WorkerA WorkerA2 WorkerC]
+ 'has_external_dependencies=true,false' | %w[WorkerA WorkerA2 WorkerB WorkerC]
+ 'has_external_dependencies=true|has_external_dependencies=false' | %w[WorkerA WorkerA2 WorkerB WorkerC]
+ 'has_external_dependencies!=true' | %w[WorkerA WorkerA2 WorkerC]
# urgency
- 'urgency=high' | %w(WorkerA2 WorkerB)
- 'urgency=low' | %w(WorkerA)
- 'urgency=high,low,throttled' | %w(WorkerA WorkerA2 WorkerB WorkerC)
- 'urgency=low|urgency=throttled' | %w(WorkerA WorkerC)
- 'urgency!=high' | %w(WorkerA WorkerC)
+ 'urgency=high' | %w[WorkerA2 WorkerB]
+ 'urgency=low' | %w[WorkerA]
+ 'urgency=high,low,throttled' | %w[WorkerA WorkerA2 WorkerB WorkerC]
+ 'urgency=low|urgency=throttled' | %w[WorkerA WorkerC]
+ 'urgency!=high' | %w[WorkerA WorkerC]
# name
- 'name=a' | %w(WorkerA)
- 'name=a,b' | %w(WorkerA WorkerB)
- 'name=a,a:2|name=b' | %w(WorkerA WorkerA2 WorkerB)
- 'name!=a,a:2' | %w(WorkerB WorkerC)
+ 'name=a' | %w[WorkerA]
+ 'name=a,b' | %w[WorkerA WorkerB]
+ 'name=a,a:2|name=b' | %w[WorkerA WorkerA2 WorkerB]
+ 'name!=a,a:2' | %w[WorkerB WorkerC]
# resource_boundary
- 'resource_boundary=memory' | %w(WorkerB WorkerC)
- 'resource_boundary=memory,cpu' | %w(WorkerA WorkerB WorkerC)
- 'resource_boundary=memory|resource_boundary=cpu' | %w(WorkerA WorkerB WorkerC)
- 'resource_boundary!=memory,cpu' | %w(WorkerA2)
+ 'resource_boundary=memory' | %w[WorkerB WorkerC]
+ 'resource_boundary=memory,cpu' | %w[WorkerA WorkerB WorkerC]
+ 'resource_boundary=memory|resource_boundary=cpu' | %w[WorkerA WorkerB WorkerC]
+ 'resource_boundary!=memory,cpu' | %w[WorkerA2]
# tags
- 'tags=no_disk_io' | %w(WorkerA WorkerB)
- 'tags=no_disk_io,git_access' | %w(WorkerA WorkerA2 WorkerB)
- 'tags=no_disk_io|tags=git_access' | %w(WorkerA WorkerA2 WorkerB)
- 'tags=no_disk_io&tags=git_access' | %w(WorkerA)
- 'tags!=no_disk_io' | %w(WorkerA2 WorkerC)
- 'tags!=no_disk_io,git_access' | %w(WorkerC)
+ 'tags=no_disk_io' | %w[WorkerA WorkerB]
+ 'tags=no_disk_io,git_access' | %w[WorkerA WorkerA2 WorkerB]
+ 'tags=no_disk_io|tags=git_access' | %w[WorkerA WorkerA2 WorkerB]
+ 'tags=no_disk_io&tags=git_access' | %w[WorkerA]
+ 'tags!=no_disk_io' | %w[WorkerA2 WorkerC]
+ 'tags!=no_disk_io,git_access' | %w[WorkerC]
'tags=unknown_tag' | []
- 'tags!=no_disk_io' | %w(WorkerA2 WorkerC)
- 'tags!=no_disk_io,git_access' | %w(WorkerC)
- 'tags!=unknown_tag' | %w(WorkerA WorkerA2 WorkerB WorkerC)
+ 'tags!=no_disk_io' | %w[WorkerA2 WorkerC]
+ 'tags!=no_disk_io,git_access' | %w[WorkerC]
+ 'tags!=unknown_tag' | %w[WorkerA WorkerA2 WorkerB WorkerC]
# combinations
- 'feature_category=category_a&urgency=high' | %w(WorkerA2)
- 'feature_category=category_a&urgency=high|feature_category=category_c' | %w(WorkerA2 WorkerC)
+ 'feature_category=category_a&urgency=high' | %w[WorkerA2]
+ 'feature_category=category_a&urgency=high|feature_category=category_c' | %w[WorkerA2 WorkerC]
# Match all
- '*' | %w(WorkerA WorkerA2 WorkerB WorkerC)
+ '*' | %w[WorkerA WorkerA2 WorkerB WorkerC]
end
with_them do
diff --git a/spec/lib/gitlab/sidekiq_logging/structured_logger_spec.rb b/spec/lib/gitlab/sidekiq_logging/structured_logger_spec.rb
index 4550ccc2fff..2e07fa100e8 100644
--- a/spec/lib/gitlab/sidekiq_logging/structured_logger_spec.rb
+++ b/spec/lib/gitlab/sidekiq_logging/structured_logger_spec.rb
@@ -181,7 +181,7 @@ RSpec.describe Gitlab::SidekiqLogging::StructuredLogger do
it 'logs without created_at and enqueued_at fields' do
travel_to(timestamp) do
- excluded_fields = %w(created_at enqueued_at args scheduling_latency_s)
+ excluded_fields = %w[created_at enqueued_at args scheduling_latency_s]
expect(logger).to receive(:info).with(start_payload.except(*excluded_fields)).ordered
expect(logger).to receive(:info).with(end_payload.except(*excluded_fields)).ordered
@@ -238,13 +238,11 @@ RSpec.describe Gitlab::SidekiqLogging::StructuredLogger do
end
end
- context 'with Gitaly, Rugged, and Redis calls' do
+ context 'with Gitaly, and Redis calls' do
let(:timing_data) do
{
gitaly_calls: 10,
gitaly_duration_s: 10000,
- rugged_calls: 1,
- rugged_duration_s: 5000,
redis_calls: 3,
redis_duration_s: 1234
}
@@ -261,7 +259,7 @@ RSpec.describe Gitlab::SidekiqLogging::StructuredLogger do
end
end
- it 'logs with Gitaly and Rugged timing data', :aggregate_failures do
+ it 'logs with Gitaly timing data', :aggregate_failures do
travel_to(timestamp) do
expect(logger).to receive(:info).with(start_payload).ordered
expect(logger).to receive(:info).with(expected_end_payload).ordered
diff --git a/spec/lib/gitlab/sidekiq_middleware/duplicate_jobs/duplicate_job_spec.rb b/spec/lib/gitlab/sidekiq_middleware/duplicate_jobs/duplicate_job_spec.rb
index 7138ad04f69..dbfab116479 100644
--- a/spec/lib/gitlab/sidekiq_middleware/duplicate_jobs/duplicate_job_spec.rb
+++ b/spec/lib/gitlab/sidekiq_middleware/duplicate_jobs/duplicate_job_spec.rb
@@ -106,9 +106,21 @@ RSpec.describe Gitlab::SidekiqMiddleware::DuplicateJobs::DuplicateJob,
end
context 'when TTL option is not set' do
- let(:expected_ttl) { described_class::DEFAULT_DUPLICATE_KEY_TTL }
+ context 'when reduce_duplicate_job_key_ttl is enabled' do
+ let(:expected_ttl) { described_class::SHORT_DUPLICATE_KEY_TTL }
- it_behaves_like 'sets Redis keys with correct TTL'
+ it_behaves_like 'sets Redis keys with correct TTL'
+ end
+
+ context 'when reduce_duplicate_job_key_ttl is disabled' do
+ before do
+ stub_feature_flags(reduce_duplicate_job_key_ttl: false)
+ end
+
+ let(:expected_ttl) { described_class::DEFAULT_DUPLICATE_KEY_TTL }
+
+ it_behaves_like 'sets Redis keys with correct TTL'
+ end
end
context 'when TTL option is set' do
diff --git a/spec/lib/gitlab/sidekiq_middleware/server_metrics_spec.rb b/spec/lib/gitlab/sidekiq_middleware/server_metrics_spec.rb
index a27e723e392..9cf9901007c 100644
--- a/spec/lib/gitlab/sidekiq_middleware/server_metrics_spec.rb
+++ b/spec/lib/gitlab/sidekiq_middleware/server_metrics_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
# rubocop: disable RSpec/MultipleMemoizedHelpers
-RSpec.describe Gitlab::SidekiqMiddleware::ServerMetrics do
+RSpec.describe Gitlab::SidekiqMiddleware::ServerMetrics, feature_category: :shared do
shared_examples "a metrics middleware" do
context "with mocked prometheus" do
include_context 'server metrics with mocked prometheus'
@@ -452,11 +452,6 @@ RSpec.describe Gitlab::SidekiqMiddleware::ServerMetrics do
end
context 'when emit_sidekiq_histogram_metrics FF is disabled' do
- include_context 'server metrics with mocked prometheus'
- include_context 'server metrics call' do
- let(:stub_subject) { false }
- end
-
subject(:middleware) { described_class.new }
let(:job) { {} }
@@ -484,16 +479,38 @@ RSpec.describe Gitlab::SidekiqMiddleware::ServerMetrics do
stub_feature_flags(emit_sidekiq_histogram_metrics: false)
end
+ # include_context below must run after stubbing FF above because
+ # the middleware initialization depends on the FF and it's being initialized
+ # in the 'server metrics call' shared_context
+ include_context 'server metrics with mocked prometheus'
+ include_context 'server metrics call'
+
it 'does not emit histogram metrics' do
expect(completion_seconds_metric).not_to receive(:observe)
expect(queue_duration_seconds).not_to receive(:observe)
expect(failed_total_metric).not_to receive(:increment)
+ expect(user_execution_seconds_metric).not_to receive(:observe)
+ expect(db_seconds_metric).not_to receive(:observe)
+ expect(gitaly_seconds_metric).not_to receive(:observe)
+ expect(redis_seconds_metric).not_to receive(:observe)
+ expect(elasticsearch_seconds_metric).not_to receive(:observe)
middleware.call(worker, job, queue) { nil }
end
- it 'emits sidekiq_jobs_completion_seconds_sum metric' do
+ it 'emits sidekiq_jobs_completion_seconds sum and count metric' do
expect(completion_seconds_sum_metric).to receive(:increment).with(labels, monotonic_time_duration)
+ expect(completion_count_metric).to receive(:increment).with(labels, 1)
+
+ middleware.call(worker, job, queue) { nil }
+ end
+
+ it 'emits resource usage sum metrics' do
+ expect(cpu_seconds_sum_metric).to receive(:increment).with(labels, thread_cputime_duration)
+ expect(db_seconds_sum_metric).to receive(:increment).with(labels, db_duration)
+ expect(gitaly_seconds_sum_metric).to receive(:increment).with(labels, gitaly_duration)
+ expect(redis_seconds_sum_metric).to receive(:increment).with(labels, redis_duration)
+ expect(elasticsearch_seconds_sum_metric).to receive(:increment).with(labels, elasticsearch_duration)
middleware.call(worker, job, queue) { nil }
end
diff --git a/spec/lib/gitlab/sidekiq_middleware/skip_jobs_spec.rb b/spec/lib/gitlab/sidekiq_middleware/skip_jobs_spec.rb
index 2fa0e44d44f..6df77c350e2 100644
--- a/spec/lib/gitlab/sidekiq_middleware/skip_jobs_spec.rb
+++ b/spec/lib/gitlab/sidekiq_middleware/skip_jobs_spec.rb
@@ -185,6 +185,21 @@ RSpec.describe Gitlab::SidekiqMiddleware::SkipJobs, feature_category: :scalabili
TestWorker.perform_async(*job['args'])
end
end
+
+ context 'when a block is provided' do
+ before do
+ TestWorker.defer_on_database_health_signal(*health_signal_attrs.values) do
+ [:gitlab_ci, [:ci_pipelines]]
+ end
+ end
+
+ it 'uses the lazy evaluated schema and tables returned by the block' do
+ expect(Gitlab::Database::HealthStatus::Context).to receive(:new)
+ .with(anything, anything, [:ci_pipelines], :gitlab_ci).and_call_original
+
+ expect { |b| subject.call(TestWorker.new, job, queue, &b) }.to yield_control
+ end
+ end
end
end
end
diff --git a/spec/lib/gitlab/sidekiq_middleware/worker_context/client_spec.rb b/spec/lib/gitlab/sidekiq_middleware/worker_context/client_spec.rb
index 4fbc64a45d6..0f8d84d13ec 100644
--- a/spec/lib/gitlab/sidekiq_middleware/worker_context/client_spec.rb
+++ b/spec/lib/gitlab/sidekiq_middleware/worker_context/client_spec.rb
@@ -73,7 +73,7 @@ RSpec.describe Gitlab::SidekiqMiddleware::WorkerContext::Client do
'job2' => build_stubbed(:user, username: 'user-2') }
TestWithContextWorker.bulk_perform_async_with_contexts(
- %w(job1 job2),
+ %w[job1 job2],
arguments_proc: -> (name) { [name, 1, 2, 3] },
context_proc: -> (name) { { user: user_per_job[name] } }
)
@@ -88,7 +88,7 @@ RSpec.describe Gitlab::SidekiqMiddleware::WorkerContext::Client do
context 'when the feature category is set in the context_proc' do
it 'takes the feature category from the worker, not the caller' do
TestWithContextWorker.bulk_perform_async_with_contexts(
- %w(job1 job2),
+ %w[job1 job2],
arguments_proc: -> (name) { [name, 1, 2, 3] },
context_proc: -> (_) { { feature_category: 'code_review' } }
)
@@ -102,7 +102,7 @@ RSpec.describe Gitlab::SidekiqMiddleware::WorkerContext::Client do
it 'takes the feature category from the caller if the worker is not owned' do
TestNotOwnedWithContextWorker.bulk_perform_async_with_contexts(
- %w(job1 job2),
+ %w[job1 job2],
arguments_proc: -> (name) { [name, 1, 2, 3] },
context_proc: -> (_) { { feature_category: 'code_review' } }
)
@@ -125,7 +125,7 @@ RSpec.describe Gitlab::SidekiqMiddleware::WorkerContext::Client do
it 'takes the feature category from the worker, not the caller' do
Gitlab::ApplicationContext.with_context(feature_category: 'system_access') do
TestWithContextWorker.bulk_perform_async_with_contexts(
- %w(job1 job2),
+ %w[job1 job2],
arguments_proc: -> (name) { [name, 1, 2, 3] },
context_proc: -> (_) { {} }
)
@@ -141,7 +141,7 @@ RSpec.describe Gitlab::SidekiqMiddleware::WorkerContext::Client do
it 'takes the feature category from the caller if the worker is not owned' do
Gitlab::ApplicationContext.with_context(feature_category: 'system_access') do
TestNotOwnedWithContextWorker.bulk_perform_async_with_contexts(
- %w(job1 job2),
+ %w[job1 job2],
arguments_proc: -> (name) { [name, 1, 2, 3] },
context_proc: -> (_) { {} }
)
diff --git a/spec/lib/gitlab/sidekiq_status_spec.rb b/spec/lib/gitlab/sidekiq_status_spec.rb
index 7f1504a8df9..a555e6a828a 100644
--- a/spec/lib/gitlab/sidekiq_status_spec.rb
+++ b/spec/lib/gitlab/sidekiq_status_spec.rb
@@ -55,13 +55,13 @@ RSpec.describe Gitlab::SidekiqStatus, :clean_gitlab_redis_queues, :clean_gitlab_
describe '.all_completed?' do
it 'returns true if all jobs have been completed' do
- expect(described_class.all_completed?(%w(123))).to eq(true)
+ expect(described_class.all_completed?(%w[123])).to eq(true)
end
it 'returns false if a job has not yet been completed' do
described_class.set('123')
- expect(described_class.all_completed?(%w(123 456))).to eq(false)
+ expect(described_class.all_completed?(%w[123 456])).to eq(false)
end
end
@@ -79,40 +79,40 @@ RSpec.describe Gitlab::SidekiqStatus, :clean_gitlab_redis_queues, :clean_gitlab_
describe '.num_running' do
it 'returns 0 if all jobs have been completed' do
- expect(described_class.num_running(%w(123))).to eq(0)
+ expect(described_class.num_running(%w[123])).to eq(0)
end
it 'returns 2 if two jobs are still running' do
described_class.set('123')
described_class.set('456')
- expect(described_class.num_running(%w(123 456 789))).to eq(2)
+ expect(described_class.num_running(%w[123 456 789])).to eq(2)
end
end
describe '.num_completed' do
it 'returns 1 if all jobs have been completed' do
- expect(described_class.num_completed(%w(123))).to eq(1)
+ expect(described_class.num_completed(%w[123])).to eq(1)
end
it 'returns 1 if a job has not yet been completed' do
described_class.set('123')
described_class.set('456')
- expect(described_class.num_completed(%w(123 456 789))).to eq(1)
+ expect(described_class.num_completed(%w[123 456 789])).to eq(1)
end
end
describe '.completed_jids' do
it 'returns the completed job' do
- expect(described_class.completed_jids(%w(123))).to eq(['123'])
+ expect(described_class.completed_jids(%w[123])).to eq(['123'])
end
it 'returns only the jobs completed' do
described_class.set('123')
described_class.set('456')
- expect(described_class.completed_jids(%w(123 456 789))).to eq(['789'])
+ expect(described_class.completed_jids(%w[123 456 789])).to eq(['789'])
end
end
@@ -122,7 +122,7 @@ RSpec.describe Gitlab::SidekiqStatus, :clean_gitlab_redis_queues, :clean_gitlab_
described_class.set('456')
described_class.unset('123')
- expect(described_class.job_status(%w(123 456 789))).to eq([false, true, false])
+ expect(described_class.job_status(%w[123 456 789])).to eq([false, true, false])
end
it 'handles an empty array' do
@@ -140,7 +140,7 @@ RSpec.describe Gitlab::SidekiqStatus, :clean_gitlab_redis_queues, :clean_gitlab_
expect(Gitlab::Redis::SidekiqStatus).to receive(:with).and_call_original
expect(Sidekiq).not_to receive(:redis)
- described_class.job_status(%w(123 456 789))
+ described_class.job_status(%w[123 456 789])
end
it_behaves_like 'tracking status in redis'
@@ -160,7 +160,7 @@ RSpec.describe Gitlab::SidekiqStatus, :clean_gitlab_redis_queues, :clean_gitlab_
expect(Sidekiq).to receive(:redis).and_call_original
expect(Gitlab::Redis::SidekiqStatus).not_to receive(:with)
- described_class.job_status(%w(123 456 789))
+ described_class.job_status(%w[123 456 789])
end
it_behaves_like 'tracking status in redis'
diff --git a/spec/lib/gitlab/ssh_public_key_spec.rb b/spec/lib/gitlab/ssh_public_key_spec.rb
index d4b0b1ea53b..df9f04eb7a0 100644
--- a/spec/lib/gitlab/ssh_public_key_spec.rb
+++ b/spec/lib/gitlab/ssh_public_key_spec.rb
@@ -87,28 +87,28 @@ RSpec.describe Gitlab::SSHPublicKey, lib: true, fips_mode: false do
describe '.supported_algorithms' do
it 'returns all supported algorithms' do
expect(described_class.supported_algorithms).to eq(
- %w(
+ %w[
ssh-rsa
ssh-dss
ecdsa-sha2-nistp256 ecdsa-sha2-nistp384 ecdsa-sha2-nistp521
ssh-ed25519
sk-ecdsa-sha2-nistp256@openssh.com
sk-ssh-ed25519@openssh.com
- )
+ ]
)
end
context 'FIPS mode', :fips_mode do
it 'returns all supported algorithms' do
expect(described_class.supported_algorithms).to eq(
- %w(
+ %w[
ssh-rsa
ssh-dss
ecdsa-sha2-nistp256 ecdsa-sha2-nistp384 ecdsa-sha2-nistp521
ssh-ed25519
sk-ecdsa-sha2-nistp256@openssh.com
sk-ssh-ed25519@openssh.com
- )
+ ]
)
end
end
@@ -117,12 +117,12 @@ RSpec.describe Gitlab::SSHPublicKey, lib: true, fips_mode: false do
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)],
- [:ecdsa_sk, %w(sk-ecdsa-sha2-nistp256@openssh.com)],
- [:ed25519_sk, %w(sk-ssh-ed25519@openssh.com)]
+ [:rsa, %w[ssh-rsa]],
+ [:dsa, %w[ssh-dss]],
+ [:ecdsa, %w[ecdsa-sha2-nistp256 ecdsa-sha2-nistp384 ecdsa-sha2-nistp521]],
+ [:ed25519, %w[ssh-ed25519]],
+ [:ecdsa_sk, %w[sk-ecdsa-sha2-nistp256@openssh.com]],
+ [:ed25519_sk, %w[sk-ssh-ed25519@openssh.com]]
]
end
@@ -136,12 +136,12 @@ RSpec.describe Gitlab::SSHPublicKey, lib: true, fips_mode: false do
context 'FIPS mode', :fips_mode 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)],
- [:ecdsa_sk, %w(sk-ecdsa-sha2-nistp256@openssh.com)],
- [:ed25519_sk, %w(sk-ssh-ed25519@openssh.com)]
+ [:rsa, %w[ssh-rsa]],
+ [:dsa, %w[ssh-dss]],
+ [:ecdsa, %w[ecdsa-sha2-nistp256 ecdsa-sha2-nistp384 ecdsa-sha2-nistp521]],
+ [:ed25519, %w[ssh-ed25519]],
+ [:ecdsa_sk, %w[sk-ecdsa-sha2-nistp256@openssh.com]],
+ [:ed25519_sk, %w[sk-ssh-ed25519@openssh.com]]
]
end
@@ -194,7 +194,7 @@ RSpec.describe Gitlab::SSHPublicKey, lib: true, fips_mode: false do
context 'with a valid SSH key' do
where(:factory) do
- %i(rsa_key_2048
+ %i[rsa_key_2048
rsa_key_4096
rsa_key_5120
rsa_key_8192
@@ -202,7 +202,7 @@ RSpec.describe Gitlab::SSHPublicKey, lib: true, fips_mode: false do
ecdsa_key_256
ed25519_key_256
ecdsa_sk_key_256
- ed25519_sk_key_256)
+ ed25519_sk_key_256]
end
with_them do
diff --git a/spec/lib/gitlab/string_range_marker_spec.rb b/spec/lib/gitlab/string_range_marker_spec.rb
index 2ababd6a938..fcee64bc01c 100644
--- a/spec/lib/gitlab/string_range_marker_spec.rb
+++ b/spec/lib/gitlab/string_range_marker_spec.rb
@@ -14,10 +14,10 @@ RSpec.describe Gitlab::StringRangeMarker do
end
context "when the rich text is html safe" do
- let(:rich) { %{<span class="abc">abc</span><span class="space"> </span><span class="def">&lt;def&gt;</span>}.html_safe }
+ let(:rich) { %(<span class="abc">abc</span><span class="space"> </span><span class="def">&lt;def&gt;</span>).html_safe }
it 'marks the inline diffs' do
- expect(mark_diff(rich)).to eq(%{<span class="abc">abLEFTcRIGHT</span><span class="space">LEFT RIGHT</span><span class="def">LEFT&lt;dRIGHTef&gt;</span>})
+ expect(mark_diff(rich)).to eq(%(<span class="abc">abLEFTcRIGHT</span><span class="space">LEFT RIGHT</span><span class="def">LEFT&lt;dRIGHTef&gt;</span>))
expect(mark_diff(rich)).to be_html_safe
end
end
@@ -25,7 +25,7 @@ RSpec.describe Gitlab::StringRangeMarker do
context "when the rich text is not html safe" do
context 'when rich text equals raw text' do
it 'marks the inline diffs' do
- expect(mark_diff).to eq(%{abLEFTc <dRIGHTef>})
+ expect(mark_diff).to eq(%(abLEFTc <dRIGHTef>))
expect(mark_diff).not_to be_html_safe
end
end
@@ -34,7 +34,7 @@ RSpec.describe Gitlab::StringRangeMarker do
let(:rich) { "abc <def> differs" }
it 'marks the inline diffs' do
- expect(mark_diff(rich)).to eq(%{abLEFTc &lt;dRIGHTef&gt; differs})
+ expect(mark_diff(rich)).to eq(%(abLEFTc &lt;dRIGHTef&gt; differs))
expect(mark_diff(rich)).to be_html_safe
end
end
diff --git a/spec/lib/gitlab/string_regex_marker_spec.rb b/spec/lib/gitlab/string_regex_marker_spec.rb
index 393bfea7c6b..87df8b9baab 100644
--- a/spec/lib/gitlab/string_regex_marker_spec.rb
+++ b/spec/lib/gitlab/string_regex_marker_spec.rb
@@ -5,34 +5,34 @@ require 'fast_spec_helper'
RSpec.describe Gitlab::StringRegexMarker do
describe '#mark' do
context 'with a single occurrence' do
- let(:raw) { %{"name": "AFNetworking"} }
- let(:rich) { %{<span class="key">"name"</span><span class="punctuation">: </span><span class="value">"AFNetworking"</span>}.html_safe }
+ let(:raw) { %("name": "AFNetworking") }
+ let(:rich) { %(<span class="key">"name"</span><span class="punctuation">: </span><span class="value">"AFNetworking"</span>).html_safe }
subject do
described_class.new(raw, rich).mark(/"[^"]+":\s*"(?<name>[^"]+)"/, group: :name) do |text, left:, right:, mode:|
- %{<a href="#">#{text}</a>}.html_safe
+ %(<a href="#">#{text}</a>).html_safe
end
end
it 'marks the match' do
- expect(subject).to eq(%{<span class="key">"name"</span><span class="punctuation">: </span><span class="value">"<a href="#">AFNetworking</a>"</span>})
+ expect(subject).to eq(%(<span class="key">"name"</span><span class="punctuation">: </span><span class="value">"<a href="#">AFNetworking</a>"</span>))
expect(subject).to be_html_safe
end
end
context 'with multiple occurrences' do
- let(:raw) { %{a <b> <c> d} }
- let(:rich) { %{a &lt;b&gt; &lt;c&gt; d}.html_safe }
+ let(:raw) { %(a <b> <c> d) }
+ let(:rich) { %(a &lt;b&gt; &lt;c&gt; d).html_safe }
let(:regexp) { /<[a-z]>/ }
subject do
described_class.new(raw, rich).mark(regexp) do |text, left:, right:, mode:|
- %{<strong>#{text}</strong>}.html_safe
+ %(<strong>#{text}</strong>).html_safe
end
end
it 'marks the matches' do
- expect(subject).to eq(%{a <strong>&lt;b&gt;</strong> <strong>&lt;c&gt;</strong> d})
+ expect(subject).to eq(%(a <strong>&lt;b&gt;</strong> <strong>&lt;c&gt;</strong> d))
expect(subject).to be_html_safe
end
@@ -40,7 +40,7 @@ RSpec.describe Gitlab::StringRegexMarker do
let(:regexp) { Gitlab::UntrustedRegexp.new('<[a-z]>') }
it 'marks the matches' do
- expect(subject).to eq(%{a <strong>&lt;b&gt;</strong> <strong>&lt;c&gt;</strong> d})
+ expect(subject).to eq(%(a <strong>&lt;b&gt;</strong> <strong>&lt;c&gt;</strong> d))
expect(subject).to be_html_safe
end
end
diff --git a/spec/lib/gitlab/suggestions/suggestion_set_spec.rb b/spec/lib/gitlab/suggestions/suggestion_set_spec.rb
index 469646986e1..298ade2e33f 100644
--- a/spec/lib/gitlab/suggestions/suggestion_set_spec.rb
+++ b/spec/lib/gitlab/suggestions/suggestion_set_spec.rb
@@ -114,7 +114,7 @@ RSpec.describe Gitlab::Suggestions::SuggestionSet do
it 'returns an array of unique file paths associated with the suggestions' do
suggestion_set = described_class.new([suggestion, suggestion2, suggestion3])
- expected_paths = %w(files/ruby/popen.rb files/ruby/regex.rb)
+ expected_paths = %w[files/ruby/popen.rb files/ruby/regex.rb]
actual_paths = suggestion_set.file_paths
diff --git a/spec/lib/gitlab/task_helpers_spec.rb b/spec/lib/gitlab/task_helpers_spec.rb
index 0c43dd15e8c..448406dfb99 100644
--- a/spec/lib/gitlab/task_helpers_spec.rb
+++ b/spec/lib/gitlab/task_helpers_spec.rb
@@ -84,17 +84,17 @@ RSpec.describe Gitlab::TaskHelpers do
describe '#run_command' do
it 'runs command and return the output' do
- expect(subject.run_command(%w(echo it works!))).to eq("it works!\n")
+ expect(subject.run_command(%w[echo it works!])).to eq("it works!\n")
end
it 'returns empty string when command doesnt exist' do
- expect(subject.run_command(%w(nonexistentcommand with arguments))).to eq('')
+ expect(subject.run_command(%w[nonexistentcommand with arguments])).to eq('')
end
end
describe '#run_command!' do
it 'runs command and return the output' do
- expect(subject.run_command!(%w(echo it works!))).to eq("it works!\n")
+ expect(subject.run_command!(%w[echo it works!])).to eq("it works!\n")
end
it 'returns and exception when command exit with non zero code' do
diff --git a/spec/lib/gitlab/tracking/event_definition_spec.rb b/spec/lib/gitlab/tracking/event_definition_spec.rb
index b27aaa35695..ab0660147e4 100644
--- a/spec/lib/gitlab/tracking/event_definition_spec.rb
+++ b/spec/lib/gitlab/tracking/event_definition_spec.rb
@@ -15,8 +15,8 @@ RSpec.describe Gitlab::Tracking::EventDefinition do
product_stage: 'growth',
product_section: 'dev',
product_group: 'group::product analytics',
- distribution: %w(ee ce),
- tier: %w(free premium ultimate)
+ distribution: %w[ee ce],
+ tier: %w[free premium ultimate]
}
end
@@ -49,8 +49,8 @@ RSpec.describe Gitlab::Tracking::EventDefinition do
:product_stage | 1
:product_section | nil
:product_group | nil
- :distributions | %[be eb]
- :tiers | %[pro]
+ :distributions | %(be eb)
+ :tiers | %(pro)
end
with_them do
diff --git a/spec/lib/gitlab/url_builder_spec.rb b/spec/lib/gitlab/url_builder_spec.rb
index 68eb38a1335..81b70f85c3a 100644
--- a/spec/lib/gitlab/url_builder_spec.rb
+++ b/spec/lib/gitlab/url_builder_spec.rb
@@ -30,6 +30,7 @@ RSpec.describe Gitlab::UrlBuilder do
:project_snippet | ->(snippet) { "/#{snippet.project.full_path}/-/snippets/#{snippet.id}" }
:project_wiki | ->(wiki) { "/#{wiki.container.full_path}/-/wikis/home" }
:release | ->(release) { "/#{release.project.full_path}/-/releases/#{release.tag}" }
+ :organization | ->(organization) { "/-/organizations/#{organization.path}" }
:ci_build | ->(build) { "/#{build.project.full_path}/-/jobs/#{build.id}" }
:design | ->(design) { "/#{design.project.full_path}/-/design_management/designs/#{design.id}/raw_image" }
diff --git a/spec/lib/gitlab/url_sanitizer_spec.rb b/spec/lib/gitlab/url_sanitizer_spec.rb
index 2c2ef8f13fb..6a1521d9d72 100644
--- a/spec/lib/gitlab/url_sanitizer_spec.rb
+++ b/spec/lib/gitlab/url_sanitizer_spec.rb
@@ -8,10 +8,10 @@ RSpec.describe Gitlab::UrlSanitizer do
describe '.sanitize' do
def sanitize_url(url)
# We want to try with multi-line content because is how error messages are formatted
- described_class.sanitize(%{
+ described_class.sanitize(%(
remote: Not Found
fatal: repository `#{url}` not found
- })
+ ))
end
where(:input, :output) do
@@ -50,7 +50,7 @@ RSpec.describe Gitlab::UrlSanitizer do
false | '123://invalid:url'
false | 'valid@project:url.git'
false | 'valid:pass@project:url.git'
- false | %w(test array)
+ false | %w[test array]
true | 'ssh://example.com'
true | 'ssh://:@example.com'
true | 'ssh://foo@example.com'
@@ -74,7 +74,7 @@ RSpec.describe Gitlab::UrlSanitizer do
false | '123://invalid:url'
false | 'valid@project:url.git'
false | 'valid:pass@project:url.git'
- false | %w(test array)
+ false | %w[test array]
false | 'ssh://example.com'
false | 'ssh://:@example.com'
false | 'ssh://foo@example.com'
diff --git a/spec/lib/gitlab/usage/metric_definition_spec.rb b/spec/lib/gitlab/usage/metric_definition_spec.rb
index 51d3090c825..08adc031631 100644
--- a/spec/lib/gitlab/usage/metric_definition_spec.rb
+++ b/spec/lib/gitlab/usage/metric_definition_spec.rb
@@ -16,8 +16,8 @@ RSpec.describe Gitlab::Usage::MetricDefinition, feature_category: :service_ping
product_group: 'product_analytics',
time_frame: 'none',
data_source: 'database',
- distribution: %w(ee ce),
- tier: %w(free starter premium ultimate bronze silver gold),
+ distribution: %w[ee ce],
+ tier: %w[free starter premium ultimate bronze silver gold],
data_category: 'standard',
removed_by_url: 'http://gdk.test'
}
@@ -64,7 +64,7 @@ RSpec.describe Gitlab::Usage::MetricDefinition, feature_category: :service_ping
it 'includes metrics that are not removed' do
expect(described_class.not_removed.count).to eq(3)
- expect(described_class.not_removed.keys).to match_array(%w(metric1 metric2 metric3))
+ expect(described_class.not_removed.keys).to match_array(%w[metric1 metric2 metric3])
end
end
@@ -162,7 +162,7 @@ RSpec.describe Gitlab::Usage::MetricDefinition, feature_category: :service_ping
:data_source | nil
:distribution | nil
:distribution | 'test'
- :tier | %w(test ee)
+ :tier | %w[test ee]
:repair_issue_url | nil
:removed_by_url | 1
@@ -194,6 +194,156 @@ RSpec.describe Gitlab::Usage::MetricDefinition, feature_category: :service_ping
described_class.new(path, attributes).validate!
end
end
+
+ context 'when metric has removed status' do
+ before do
+ attributes[:status] = 'removed'
+ end
+
+ it 'raise dev exception when removed_by_url is not provided' do
+ attributes.delete(:removed_by_url)
+
+ expect(Gitlab::ErrorTracking).to receive(:track_and_raise_for_dev_exception).at_least(:once).with(instance_of(Gitlab::Usage::MetricDefinition::InvalidError))
+
+ described_class.new(path, attributes).validate!
+ end
+
+ it 'raises dev exception when milestone_removed is not provided' do
+ attributes.delete(:milestone_removed)
+
+ expect(Gitlab::ErrorTracking).to receive(:track_and_raise_for_dev_exception).at_least(:once).with(instance_of(Gitlab::Usage::MetricDefinition::InvalidError))
+
+ described_class.new(path, attributes).validate!
+ end
+ end
+
+ context 'internal metric' do
+ before do
+ attributes[:data_source] = 'internal_events'
+ end
+
+ where(:instrumentation_class, :options, :events, :is_valid) do
+ 'AnotherClass' | { events: ['a'] } | [{ name: 'a', unique: 'user.id' }] | false
+ nil | { events: ['a'] } | [{ name: 'a', unique: 'user.id' }] | false
+ 'RedisHLLMetric' | { events: ['a'] } | [{ name: 'a', unique: 'user.id' }] | true
+ 'RedisHLLMetric' | { events: ['a'] } | nil | false
+ 'RedisHLLMetric' | nil | [{ name: 'a', unique: 'user.id' }] | false
+ 'RedisHLLMetric' | { events: ['a'] } | [{ name: 'a', unique: 'a' }] | false
+ 'RedisHLLMetric' | { events: 'a' } | [{ name: 'a', unique: 'user.id' }] | false
+ 'RedisHLLMetric' | { events: [2] } | [{ name: 'a', unique: 'user.id' }] | false
+ 'RedisHLLMetric' | { events: ['a'], a: 'b' } | [{ name: 'a', unique: 'user.id' }] | false
+ 'RedisHLLMetric' | { events: ['a'] } | [{ name: 'a', unique: 'user.id', b: 'c' }] | false
+ 'RedisHLLMetric' | { events: ['a'] } | [{ name: 'a' }] | false
+ 'RedisHLLMetric' | { events: ['a'] } | [{ unique: 'user.id' }] | false
+ 'TotalCountMetric' | { events: ['a'] } | [{ name: 'a' }] | true
+ 'TotalCountMetric' | { events: ['a'] } | [{ name: 'a', unique: 'user.id' }] | false
+ 'TotalCountMetric' | { events: ['a'] } | nil | false
+ 'TotalCountMetric' | nil | [{ name: 'a' }] | false
+ 'TotalCountMetric' | { events: [2] } | [{ name: 'a' }] | false
+ 'TotalCountMetric' | { events: ['a'] } | [{}] | false
+ 'TotalCountMetric' | 'a' | [{ name: 'a' }] | false
+ 'TotalCountMetric' | { events: ['a'], a: 'b' } | [{ name: 'a' }] | false
+ end
+
+ with_them do
+ it 'raises dev exception when invalid' do
+ attributes[:instrumentation_class] = instrumentation_class if instrumentation_class
+ attributes[:options] = options if options
+ attributes[:events] = events if events
+
+ if is_valid
+ expect(Gitlab::ErrorTracking).not_to receive(:track_and_raise_for_dev_exception)
+ else
+ expect(Gitlab::ErrorTracking).to receive(:track_and_raise_for_dev_exception).at_least(:once).with(instance_of(Gitlab::Usage::MetricDefinition::InvalidError))
+ end
+
+ described_class.new(path, attributes).validate!
+ end
+ end
+ end
+
+ context 'Redis metric' do
+ before do
+ attributes[:data_source] = 'redis'
+ end
+
+ where(:instrumentation_class, :options, :is_valid) do
+ 'AnotherClass' | { event: 'a', widget: 'b' } | false
+ 'MergeRequestWidgetExtensionMetric' | { event: 'a', widget: 'b' } | true
+ 'MergeRequestWidgetExtensionMetric' | { event: 'a', widget: 2 } | false
+ 'MergeRequestWidgetExtensionMetric' | { event: 'a', widget: 'b', c: 'd' } | false
+ 'MergeRequestWidgetExtensionMetric' | { event: 'a' } | false
+ 'MergeRequestWidgetExtensionMetric' | { widget: 'b' } | false
+ 'RedisMetric' | { event: 'a', prefix: 'b', include_usage_prefix: true } | true
+ 'RedisMetric' | { event: 'a', prefix: nil, include_usage_prefix: true } | true
+ 'RedisMetric' | { event: 'a', prefix: 'b', include_usage_prefix: 2 } | false
+ 'RedisMetric' | { event: 'a', prefix: 'b', include_usage_prefix: true, c: 'd' } | false
+ 'RedisMetric' | { prefix: 'b', include_usage_prefix: true } | false
+ 'RedisMetric' | { event: 'a', include_usage_prefix: true } | false
+ 'RedisMetric' | { event: 'a', prefix: 'b' } | true
+ end
+
+ with_them do
+ it 'validates properly' do
+ attributes[:instrumentation_class] = instrumentation_class
+ attributes[:options] = options
+
+ if is_valid
+ expect(Gitlab::ErrorTracking).not_to receive(:track_and_raise_for_dev_exception)
+ else
+ expect(Gitlab::ErrorTracking).to receive(:track_and_raise_for_dev_exception).at_least(:once).with(instance_of(Gitlab::Usage::MetricDefinition::InvalidError))
+ end
+
+ described_class.new(path, attributes).validate!
+ end
+ end
+ end
+
+ context 'RedisHLL metric' do
+ before do
+ attributes[:data_source] = 'redis_hll'
+ end
+
+ where(:instrumentation_class, :options, :is_valid) do
+ 'AnotherClass' | { events: ['a'] } | false
+ 'RedisHLLMetric' | { events: ['a'] } | true
+ 'RedisHLLMetric' | nil | false
+ 'RedisHLLMetric' | {} | false
+ 'RedisHLLMetric' | { events: ['a'], b: 'c' } | false
+ 'RedisHLLMetric' | { events: [2] } | false
+ 'RedisHLLMetric' | { events: 'a' } | false
+ 'RedisHLLMetric' | { event: ['a'] } | false
+ 'AggregatedMetric' | { aggregate: { operator: 'OR', attribute: 'user_id' }, events: ['a'] } | true
+ 'AggregatedMetric' | { aggregate: { operator: 'AND', attribute: 'project_id' }, events: %w[b c] } | true
+ 'AggregatedMetric' | nil | false
+ 'AggregatedMetric' | {} | false
+ 'AggregatedMetric' | { aggregate: { operator: 'OR', attribute: 'user_id' }, events: ['a'], event: 'a' } | false
+ 'AggregatedMetric' | { aggregate: { operator: 'OR', attribute: 'user_id' } } | false
+ 'AggregatedMetric' | { events: ['a'] } | false
+ 'AggregatedMetric' | { aggregate: { operator: 'OR', attribute: 'user_id' }, events: 'a' } | false
+ 'AggregatedMetric' | { aggregate: 'a', events: ['a'] } | false
+ 'AggregatedMetric' | { aggregate: { operator: 'OR' }, events: ['a'] } | false
+ 'AggregatedMetric' | { aggregate: { attribute: 'user_id' }, events: ['a'] } | false
+ 'AggregatedMetric' | { aggregate: { operator: 'OR', attribute: 'user_id', a: 'b' }, events: ['a'] } | false
+ 'AggregatedMetric' | { aggregate: { operator: '???', attribute: 'user_id' }, events: ['a'] } | false
+ 'AggregatedMetric' | { aggregate: { operator: 'OR', attribute: ['user_id'] }, events: ['a'] } | false
+ end
+
+ with_them do
+ it 'validates properly' do
+ attributes[:instrumentation_class] = instrumentation_class
+ attributes[:options] = options
+
+ if is_valid
+ expect(Gitlab::ErrorTracking).not_to receive(:track_and_raise_for_dev_exception)
+ else
+ expect(Gitlab::ErrorTracking).to receive(:track_and_raise_for_dev_exception).at_least(:once).with(instance_of(Gitlab::Usage::MetricDefinition::InvalidError))
+ end
+
+ described_class.new(path, attributes).validate!
+ end
+ end
+ end
end
end
@@ -213,10 +363,10 @@ RSpec.describe Gitlab::Usage::MetricDefinition, feature_category: :service_ping
end
context 'when metric is using new format' do
- let(:attributes) { { events: [{ name: 'my_event', unique: 'user_id' }] } }
+ let(:attributes) { { events: [{ name: 'my_event', unique: 'user.id' }] } }
it 'returns a correct hash' do
- expect(definition.events).to eq({ 'my_event' => :user_id })
+ expect(definition.events).to eq({ 'my_event' => :'user.id' })
end
end
@@ -309,8 +459,8 @@ RSpec.describe Gitlab::Usage::MetricDefinition, feature_category: :service_ping
product_group: 'product_analytics',
time_frame: 'none',
data_source: 'database',
- distribution: %w(ee ce),
- tier: %w(free starter premium ultimate bronze silver gold),
+ distribution: %w[ee ce],
+ tier: %w[free starter premium ultimate bronze silver gold],
data_category: 'optional'
}
end
diff --git a/spec/lib/gitlab/usage/metric_spec.rb b/spec/lib/gitlab/usage/metric_spec.rb
index a4135b143dd..42d2f394ce3 100644
--- a/spec/lib/gitlab/usage/metric_spec.rb
+++ b/spec/lib/gitlab/usage/metric_spec.rb
@@ -18,8 +18,8 @@ RSpec.describe Gitlab::Usage::Metric do
time_frame: "all",
data_source: "database",
instrumentation_class: "CountIssuesMetric",
- distribution: %w(ce ee),
- tier: %w(free premium ultimate)
+ distribution: %w[ce ee],
+ tier: %w[free premium ultimate]
}
end
diff --git a/spec/lib/gitlab/usage/metrics/instrumentations/aggregated_metric_spec.rb b/spec/lib/gitlab/usage/metrics/instrumentations/aggregated_metric_spec.rb
index 3e7b13e21c1..f6b9da68184 100644
--- a/spec/lib/gitlab/usage/metrics/instrumentations/aggregated_metric_spec.rb
+++ b/spec/lib/gitlab/usage/metrics/instrumentations/aggregated_metric_spec.rb
@@ -2,7 +2,8 @@
require 'spec_helper'
-RSpec.describe Gitlab::Usage::Metrics::Instrumentations::AggregatedMetric, :clean_gitlab_redis_shared_state do
+RSpec.describe Gitlab::Usage::Metrics::Instrumentations::AggregatedMetric, :clean_gitlab_redis_shared_state,
+ feature_category: :service_ping do
using RSpec::Parameterized::TableSyntax
before do
# weekly AND 1 weekly OR 2
diff --git a/spec/lib/gitlab/usage/metrics/instrumentations/count_projects_with_jira_dvcs_integration_metric_spec.rb b/spec/lib/gitlab/usage/metrics/instrumentations/count_projects_with_jira_dvcs_integration_metric_spec.rb
deleted file mode 100644
index a2d86fc5044..00000000000
--- a/spec/lib/gitlab/usage/metrics/instrumentations/count_projects_with_jira_dvcs_integration_metric_spec.rb
+++ /dev/null
@@ -1,50 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Gitlab::Usage::Metrics::Instrumentations::CountProjectsWithJiraDvcsIntegrationMetric,
- feature_category: :integrations do
- describe 'metric value and query' do
- let_it_be_with_reload(:project_1) { create(:project) }
- let_it_be_with_reload(:project_2) { create(:project) }
- let_it_be_with_reload(:project_3) { create(:project) }
-
- before do
- project_1.feature_usage.log_jira_dvcs_integration_usage(cloud: false)
- project_2.feature_usage.log_jira_dvcs_integration_usage(cloud: false)
- project_3.feature_usage.log_jira_dvcs_integration_usage(cloud: true)
- end
-
- context 'when counting cloud integrations' do
- let(:expected_value) { 1 }
- let(:expected_query) do
- 'SELECT COUNT("project_feature_usages"."project_id") FROM "project_feature_usages" ' \
- 'WHERE "project_feature_usages"."jira_dvcs_cloud_last_sync_at" IS NOT NULL'
- end
-
- it_behaves_like 'a correct instrumented metric value and query', { time_frame: 'all', options: { cloud: true } }
- end
-
- context 'when counting non-cloud integrations' do
- let(:expected_value) { 2 }
- let(:expected_query) do
- 'SELECT COUNT("project_feature_usages"."project_id") FROM "project_feature_usages" ' \
- 'WHERE "project_feature_usages"."jira_dvcs_server_last_sync_at" IS NOT NULL'
- end
-
- it_behaves_like 'a correct instrumented metric value and query', { time_frame: 'all', options: { cloud: false } }
- end
- end
-
- it "raises an exception if option is not present" do
- expect do
- described_class.new(options: {}, time_frame: 'all')
- end.to raise_error(ArgumentError, %r{must be a boolean})
- end
-
- it "raises an exception if option has invalid value" do
- expect do
- described_class.new(options: { cloud: 'yes' }, time_frame: 'all')
- end.to raise_error(ArgumentError, %r{must be a boolean})
- end
-end
diff --git a/spec/lib/gitlab/usage/metrics/instrumentations/database_metric_spec.rb b/spec/lib/gitlab/usage/metrics/instrumentations/database_metric_spec.rb
index 8ca42a6f007..9fcec56d019 100644
--- a/spec/lib/gitlab/usage/metrics/instrumentations/database_metric_spec.rb
+++ b/spec/lib/gitlab/usage/metrics/instrumentations/database_metric_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::Usage::Metrics::Instrumentations::DatabaseMetric do
+RSpec.describe Gitlab::Usage::Metrics::Instrumentations::DatabaseMetric, feature_category: :service_ping do
let(:database_metric_class) { Class.new(described_class) }
subject do
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 cc4df696b37..e65d5d30d9d 100644
--- a/spec/lib/gitlab/usage/metrics/instrumentations/generic_metric_spec.rb
+++ b/spec/lib/gitlab/usage/metrics/instrumentations/generic_metric_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::Usage::Metrics::Instrumentations::GenericMetric do
+RSpec.describe Gitlab::Usage::Metrics::Instrumentations::GenericMetric, feature_category: :service_ping do
shared_examples 'custom fallback' do |custom_fallback|
subject do
Class.new(described_class) do
diff --git a/spec/lib/gitlab/usage/metrics/instrumentations/numbers_metric_spec.rb b/spec/lib/gitlab/usage/metrics/instrumentations/numbers_metric_spec.rb
index 180c76d56f3..008e30eca9c 100644
--- a/spec/lib/gitlab/usage/metrics/instrumentations/numbers_metric_spec.rb
+++ b/spec/lib/gitlab/usage/metrics/instrumentations/numbers_metric_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::Usage::Metrics::Instrumentations::NumbersMetric do
+RSpec.describe Gitlab::Usage::Metrics::Instrumentations::NumbersMetric, feature_category: :service_ping do
subject do
described_class.tap do |metric_class|
metric_class.operation :add
diff --git a/spec/lib/gitlab/usage/metrics/instrumentations/redis_hll_metric_spec.rb b/spec/lib/gitlab/usage/metrics/instrumentations/redis_hll_metric_spec.rb
index 97306051533..33868d365a5 100644
--- a/spec/lib/gitlab/usage/metrics/instrumentations/redis_hll_metric_spec.rb
+++ b/spec/lib/gitlab/usage/metrics/instrumentations/redis_hll_metric_spec.rb
@@ -2,7 +2,8 @@
require 'spec_helper'
-RSpec.describe Gitlab::Usage::Metrics::Instrumentations::RedisHLLMetric, :clean_gitlab_redis_shared_state do
+RSpec.describe Gitlab::Usage::Metrics::Instrumentations::RedisHLLMetric, :clean_gitlab_redis_shared_state,
+ feature_category: :service_ping do
before do
Gitlab::UsageDataCounters::HLLRedisCounter.track_event(:i_quickactions_approve, values: 1, time: 1.week.ago)
Gitlab::UsageDataCounters::HLLRedisCounter.track_event(:i_quickactions_approve, values: 1, time: 2.weeks.ago)
diff --git a/spec/lib/gitlab/usage/metrics/instrumentations/redis_metric_spec.rb b/spec/lib/gitlab/usage/metrics/instrumentations/redis_metric_spec.rb
index c4d6edd43e1..90568f4731e 100644
--- a/spec/lib/gitlab/usage/metrics/instrumentations/redis_metric_spec.rb
+++ b/spec/lib/gitlab/usage/metrics/instrumentations/redis_metric_spec.rb
@@ -2,7 +2,8 @@
require 'spec_helper'
-RSpec.describe Gitlab::Usage::Metrics::Instrumentations::RedisMetric, :clean_gitlab_redis_shared_state do
+RSpec.describe Gitlab::Usage::Metrics::Instrumentations::RedisMetric, :clean_gitlab_redis_shared_state,
+ feature_category: :service_ping do
before do
4.times do
Gitlab::UsageDataCounters::SourceCodeCounter.count(:pushes)
diff --git a/spec/lib/gitlab/usage/service_ping/instrumented_payload_spec.rb b/spec/lib/gitlab/usage/service_ping/instrumented_payload_spec.rb
index 9d2711c49c6..51649e389e2 100644
--- a/spec/lib/gitlab/usage/service_ping/instrumented_payload_spec.rb
+++ b/spec/lib/gitlab/usage/service_ping/instrumented_payload_spec.rb
@@ -11,7 +11,7 @@ RSpec.describe Gitlab::Usage::ServicePing::InstrumentedPayload do
end
context 'when building service ping with values' do
- let(:metrics_key_paths) { %w(counts.boards uuid redis_hll_counters.search.i_search_total_monthly) }
+ let(:metrics_key_paths) { %w[counts.boards uuid redis_hll_counters.search.i_search_total_monthly] }
let(:expected_payload) do
{
counts: { boards: 0 },
@@ -26,7 +26,7 @@ RSpec.describe Gitlab::Usage::ServicePing::InstrumentedPayload do
end
context 'when building service ping with instrumentations' do
- let(:metrics_key_paths) { %w(counts.boards uuid redis_hll_counters.search.i_search_total_monthly) }
+ let(:metrics_key_paths) { %w[counts.boards uuid redis_hll_counters.search.i_search_total_monthly] }
let(:expected_payload) do
{
counts: { boards: "SELECT COUNT(\"boards\".\"id\") FROM \"boards\"" },
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 7bef14d5f7a..a7dc0b6a060 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
@@ -113,73 +113,57 @@ RSpec.describe Gitlab::UsageDataCounters::HLLRedisCounter, :clean_gitlab_redis_s
end
end
- context 'when usage_ping is disabled' do
- it 'does not track the event' do
- allow(::ServicePing::ServicePingSettings).to receive(:enabled?).and_return(false)
+ it 'tracks event when using symbol' do
+ expect(Gitlab::Redis::HLL).to receive(:add)
- described_class.track_event(weekly_event, values: entity1, time: Date.current)
-
- expect(Gitlab::Redis::HLL).not_to receive(:add)
- end
+ described_class.track_event(:g_analytics_contribution, values: entity1)
end
- context 'when usage_ping is enabled' do
- before do
- allow(::ServicePing::ServicePingSettings).to receive(:enabled?).and_return(true)
- end
+ it 'tracks events with multiple values' do
+ values = [entity1, entity2]
+ expect(Gitlab::Redis::HLL).to receive(:add).with(key: /g_analytics_contribution/, value: values,
+ expiry: described_class::KEY_EXPIRY_LENGTH)
- it 'tracks event when using symbol' do
- expect(Gitlab::Redis::HLL).to receive(:add)
-
- described_class.track_event(:g_analytics_contribution, values: entity1)
- end
-
- it 'tracks events with multiple values' do
- values = [entity1, entity2]
- expect(Gitlab::Redis::HLL).to receive(:add).with(key: /g_analytics_contribution/, value: values,
- expiry: described_class::KEY_EXPIRY_LENGTH)
+ described_class.track_event(:g_analytics_contribution, values: values)
+ end
- described_class.track_event(:g_analytics_contribution, values: values)
- end
+ it 'raise error if metrics of unknown event' do
+ expect { described_class.track_event('unknown', values: entity1, time: Date.current) }.to raise_error(Gitlab::UsageDataCounters::HLLRedisCounter::UnknownEvent)
+ end
- it 'raise error if metrics of unknown event' do
- expect { described_class.track_event('unknown', values: entity1, time: Date.current) }.to raise_error(Gitlab::UsageDataCounters::HLLRedisCounter::UnknownEvent)
+ context 'when Rails environment is production' do
+ before do
+ allow(Rails.env).to receive(:development?).and_return(false)
+ allow(Rails.env).to receive(:test?).and_return(false)
end
- context 'when Rails environment is production' do
- before do
- allow(Rails.env).to receive(:development?).and_return(false)
- allow(Rails.env).to receive(:test?).and_return(false)
- end
-
- it 'reports only UnknownEvent exception' do
- expect(Gitlab::ErrorTracking).to receive(:track_and_raise_for_dev_exception)
- .with(Gitlab::UsageDataCounters::HLLRedisCounter::UnknownEvent)
- .once
- .and_call_original
+ it 'reports only UnknownEvent exception' do
+ expect(Gitlab::ErrorTracking).to receive(:track_and_raise_for_dev_exception)
+ .with(Gitlab::UsageDataCounters::HLLRedisCounter::UnknownEvent)
+ .once
+ .and_call_original
- expect { described_class.track_event('unknown', values: entity1, time: Date.current) }.not_to raise_error
- end
+ expect { described_class.track_event('unknown', values: entity1, time: Date.current) }.not_to raise_error
end
+ end
- it 'reports an error if Feature.enabled raise an error' do
- expect(Feature).to receive(:enabled?).and_raise(StandardError.new)
- expect(Gitlab::ErrorTracking).to receive(:track_and_raise_for_dev_exception)
+ it 'reports an error if Feature.enabled raise an error' do
+ expect(Feature).to receive(:enabled?).and_raise(StandardError.new)
+ expect(Gitlab::ErrorTracking).to receive(:track_and_raise_for_dev_exception)
- described_class.track_event(:g_analytics_contribution, values: entity1, time: Date.current)
- end
+ described_class.track_event(:g_analytics_contribution, values: entity1, time: Date.current)
+ end
- context 'for weekly events' do
- it 'sets the keys in Redis to expire' do
- described_class.track_event("g_compliance_dashboard", values: entity1)
+ context 'for weekly events' do
+ it 'sets the keys in Redis to expire' do
+ described_class.track_event("g_compliance_dashboard", values: entity1)
- Gitlab::Redis::SharedState.with do |redis|
- keys = redis.scan_each(match: "{#{described_class::REDIS_SLOT}}_g_compliance_dashboard-*").to_a
- expect(keys).not_to be_empty
+ Gitlab::Redis::SharedState.with do |redis|
+ keys = redis.scan_each(match: "{#{described_class::REDIS_SLOT}}_g_compliance_dashboard-*").to_a
+ expect(keys).not_to be_empty
- keys.each do |key|
- expect(redis.ttl(key)).to be_within(5.seconds).of(described_class::KEY_EXPIRY_LENGTH)
- end
+ keys.each do |key|
+ expect(redis.ttl(key)).to be_within(5.seconds).of(described_class::KEY_EXPIRY_LENGTH)
end
end
end
diff --git a/spec/lib/gitlab/usage_data_counters/redis_counter_spec.rb b/spec/lib/gitlab/usage_data_counters/redis_counter_spec.rb
index 753e09731bf..39d48b7b938 100644
--- a/spec/lib/gitlab/usage_data_counters/redis_counter_spec.rb
+++ b/spec/lib/gitlab/usage_data_counters/redis_counter_spec.rb
@@ -7,51 +7,19 @@ RSpec.describe Gitlab::UsageDataCounters::RedisCounter, :clean_gitlab_redis_shar
subject { Class.new.extend(described_class) }
- before do
- allow(::ServicePing::ServicePingSettings).to receive(:enabled?).and_return(service_ping_enabled)
- end
-
describe '.increment' do
- context 'when usage_ping is disabled' do
- let(:service_ping_enabled) { false }
-
- it 'counter is not increased' do
- expect do
- subject.increment(redis_key)
- end.not_to change { subject.total_count(redis_key) }
- end
- end
-
- context 'when usage_ping is enabled' do
- let(:service_ping_enabled) { true }
-
- it 'counter is increased' do
- expect do
- subject.increment(redis_key)
- end.to change { subject.total_count(redis_key) }.by(1)
- end
+ it 'counter is increased' do
+ expect do
+ subject.increment(redis_key)
+ end.to change { subject.total_count(redis_key) }.by(1)
end
end
describe '.increment_by' do
- context 'when usage_ping is disabled' do
- let(:service_ping_enabled) { false }
-
- it 'counter is not increased' do
- expect do
- subject.increment_by(redis_key, 3)
- end.not_to change { subject.total_count(redis_key) }
- end
- end
-
- context 'when usage_ping is enabled' do
- let(:service_ping_enabled) { true }
-
- it 'counter is increased' do
- expect do
- subject.increment_by(redis_key, 3)
- end.to change { subject.total_count(redis_key) }.by(3)
- end
+ it 'counter is increased' do
+ expect do
+ subject.increment_by(redis_key, 3)
+ end.to change { subject.total_count(redis_key) }.by(3)
end
end
end
diff --git a/spec/lib/gitlab/usage_data_spec.rb b/spec/lib/gitlab/usage_data_spec.rb
index 6f188aa408e..a1564318408 100644
--- a/spec/lib/gitlab/usage_data_spec.rb
+++ b/spec/lib/gitlab/usage_data_spec.rb
@@ -42,7 +42,7 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures, feature_category: :servic
end
it 'ensures recorded_at is set before any other usage data calculation' do
- %i(alt_usage_data redis_usage_data distinct_count count).each do |method|
+ %i[alt_usage_data redis_usage_data distinct_count count].each do |method|
expect(described_class).not_to receive(method)
end
expect(described_class).to receive(:recorded_at).and_raise(Exception.new('Stopped calculating recorded_at'))
@@ -191,7 +191,7 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures, feature_category: :servic
omniauth:
{ providers: omniauth_providers }
)
- allow(Devise).to receive(:omniauth_providers).and_return(%w(ldapmain ldapsecondary group_saml))
+ allow(Devise).to receive(:omniauth_providers).and_return(%w[ldapmain ldapsecondary group_saml])
for_defined_days_back do
user = create(:user)
@@ -268,7 +268,7 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures, feature_category: :servic
for_defined_days_back do
user = create(:user)
- %w(gitlab_project github bitbucket bitbucket_server gitea git manifest fogbugz).each do |type|
+ %w[gitlab_project github bitbucket bitbucket_server gitea git manifest fogbugz].each do |type|
create(:project, import_type: type, creator_id: user.id)
end
@@ -734,7 +734,7 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures, feature_category: :servic
subject { described_class.object_store_usage_data }
it 'fetches object store config of five components' do
- %w(artifacts external_diffs lfs uploads packages).each do |component|
+ %w[artifacts external_diffs lfs uploads packages].each do |component|
expect(described_class).to receive(:object_store_config).with(component).and_return("#{component}_object_store_config")
end
diff --git a/spec/lib/gitlab/utils/log_limited_array_spec.rb b/spec/lib/gitlab/utils/log_limited_array_spec.rb
index a55a176be48..23cca4fd791 100644
--- a/spec/lib/gitlab/utils/log_limited_array_spec.rb
+++ b/spec/lib/gitlab/utils/log_limited_array_spec.rb
@@ -13,7 +13,7 @@ RSpec.describe Gitlab::Utils::LogLimitedArray do
context 'when the argument is an array' do
context 'when the array is under the limit' do
it 'returns the array unchanged' do
- expect(described_class.log_limited_array(%w(a b))).to eq(%w(a b))
+ expect(described_class.log_limited_array(%w[a b])).to eq(%w[a b])
end
end
diff --git a/spec/lib/gitlab/webpack/graphql_known_operations_spec.rb b/spec/lib/gitlab/webpack/graphql_known_operations_spec.rb
index 89cade82fe6..6c3e3b4eb69 100644
--- a/spec/lib/gitlab/webpack/graphql_known_operations_spec.rb
+++ b/spec/lib/gitlab/webpack/graphql_known_operations_spec.rb
@@ -30,7 +30,7 @@ RSpec.describe Gitlab::Webpack::GraphqlKnownOperations do
2.times { ::Gitlab::Webpack::GraphqlKnownOperations.load }
- expect(::Gitlab::Webpack::GraphqlKnownOperations.load).to eq(%w(hello world test))
+ expect(::Gitlab::Webpack::GraphqlKnownOperations.load).to eq(%w[hello world test])
end
end
diff --git a/spec/lib/gitlab/wiki_pages/front_matter_parser_spec.rb b/spec/lib/gitlab/wiki_pages/front_matter_parser_spec.rb
index 3152dc2ad2f..3d165f7d830 100644
--- a/spec/lib/gitlab/wiki_pages/front_matter_parser_spec.rb
+++ b/spec/lib/gitlab/wiki_pages/front_matter_parser_spec.rb
@@ -24,7 +24,7 @@ RSpec.describe Gitlab::WikiPages::FrontMatterParser do
end
def have_correct_front_matter
- include(a: 1, b: 2, c: %w(foo bar))
+ include(a: 1, b: 2, c: %w[foo bar])
end
describe '#parse' do
diff --git a/spec/lib/gitlab/workhorse_spec.rb b/spec/lib/gitlab/workhorse_spec.rb
index cca18cb05c7..d77763f89be 100644
--- a/spec/lib/gitlab/workhorse_spec.rb
+++ b/spec/lib/gitlab/workhorse_spec.rb
@@ -226,7 +226,8 @@ RSpec.describe Gitlab::Workhorse, feature_category: :shared do
GL_ID: "user-#{user.id}",
GL_USERNAME: user.username,
GL_REPOSITORY: "project-#{project.id}",
- ShowAllRefs: false
+ ShowAllRefs: false,
+ NeedAudit: false
}
end
@@ -277,6 +278,12 @@ RSpec.describe Gitlab::Workhorse, feature_category: :shared do
it { is_expected.to include(ShowAllRefs: true) }
end
+ context 'need_audit enabled' do
+ subject { described_class.git_http_ok(repository, Gitlab::GlRepository::PROJECT, user, action, show_all_refs: true, need_audit: true) }
+
+ it { is_expected.to include(NeedAudit: true) }
+ end
+
context 'when a feature flag is set for a single project' do
before do
stub_feature_flags(gitaly_mep_mep: project)
diff --git a/spec/lib/object_storage/config_spec.rb b/spec/lib/object_storage/config_spec.rb
index 412fcb9b6b8..bf9aeb51cda 100644
--- a/spec/lib/object_storage/config_spec.rb
+++ b/spec/lib/object_storage/config_spec.rb
@@ -155,7 +155,7 @@ RSpec.describe ObjectStorage::Config, feature_category: :shared do
it { expect(subject.aws_server_side_encryption_enabled?).to be true }
it { expect(subject.server_side_encryption).to eq('AES256') }
it { expect(subject.server_side_encryption_kms_key_id).to eq('arn:aws:12345') }
- it { expect(subject.fog_attributes.keys).to match_array(%w(x-amz-server-side-encryption x-amz-server-side-encryption-aws-kms-key-id)) }
+ it { expect(subject.fog_attributes.keys).to match_array(%w[x-amz-server-side-encryption x-amz-server-side-encryption-aws-kms-key-id]) }
end
context 'with only server side encryption enabled' do
diff --git a/spec/lib/object_storage/direct_upload_spec.rb b/spec/lib/object_storage/direct_upload_spec.rb
index 3a42e6ebd09..5df295e73d7 100644
--- a/spec/lib/object_storage/direct_upload_spec.rb
+++ b/spec/lib/object_storage/direct_upload_spec.rb
@@ -123,7 +123,7 @@ RSpec.describe ObjectStorage::DirectUpload, feature_category: :shared do
expect(s3_config[:Region]).to eq(region)
expect(s3_config[:PathStyle]).to eq(path_style)
expect(s3_config[:UseIamProfile]).to eq(use_iam_profile)
- expect(s3_config.keys).not_to include(%i(ServerSideEncryption SSEKMSKeyID))
+ expect(s3_config.keys).not_to include(%i[ServerSideEncryption SSEKMSKeyID])
end
context 'when no region is specified' do
diff --git a/spec/lib/peek/views/rugged_spec.rb b/spec/lib/peek/views/rugged_spec.rb
deleted file mode 100644
index 31418b5fc81..00000000000
--- a/spec/lib/peek/views/rugged_spec.rb
+++ /dev/null
@@ -1,42 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Peek::Views::Rugged, :request_store do
- subject { described_class.new }
-
- let(:project) { create(:project) }
-
- before do
- allow(Gitlab::PerformanceBar).to receive(:enabled_for_request?).and_return(true)
- end
-
- it 'returns no results' do
- expect(subject.results).to eq({})
- end
-
- it 'returns aggregated results' do
- ::Gitlab::RuggedInstrumentation.add_query_time(1.234)
- ::Gitlab::RuggedInstrumentation.increment_query_count
- ::Gitlab::RuggedInstrumentation.increment_query_count
-
- ::Gitlab::RuggedInstrumentation.add_call_details(feature: :rugged_test,
- args: [project.repository.raw, 'HEAD'],
- duration: 0.123)
- ::Gitlab::RuggedInstrumentation.add_call_details(feature: :rugged_test2,
- args: [project.repository, 'refs/heads/master'],
- duration: 0.456)
-
- results = subject.results
- expect(results[:calls]).to eq(2)
- expect(results[:duration]).to eq('1234.00ms')
- expect(results[:details].count).to eq(2)
-
- expected = [
- [project.repository.raw.to_s, "HEAD"],
- [project.repository.to_s, "refs/heads/master"]
- ]
-
- expect(results[:details].map { |data| data[:args] }).to match_array(expected)
- end
-end
diff --git a/spec/lib/result_spec.rb b/spec/lib/result_spec.rb
index 2b88521fe14..170a2f5e777 100644
--- a/spec/lib/result_spec.rb
+++ b/spec/lib/result_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'fast_spec_helper'
+require 'spec_helper'
# NOTE:
# This spec is intended to serve as documentation examples of idiomatic usage for the `Result` type.
diff --git a/spec/lib/rouge/formatters/html_gitlab_spec.rb b/spec/lib/rouge/formatters/html_gitlab_spec.rb
index 6fc1b395fc8..5e5075b72b8 100644
--- a/spec/lib/rouge/formatters/html_gitlab_spec.rb
+++ b/spec/lib/rouge/formatters/html_gitlab_spec.rb
@@ -15,14 +15,14 @@ RSpec.describe Rouge::Formatters::HTMLGitlab, feature_category: :source_code_man
let(:options) { { tag: lang, ellipsis_indexes: [0], ellipsis_svg: "svg_icon" } }
it 'returns highlighted ruby code with svg' do
- code = %q{<span id="LC1" class="line" lang="ruby"><span class="k">def</span> <span class="nf">hello</span><span class="gl-px-2 gl-rounded-base gl-mx-2 gl-bg-gray-100 gl-cursor-help has-tooltip" title="Content has been trimmed">svg_icon</span></span>}
+ code = %q(<span id="LC1" class="line" lang="ruby"><span class="k">def</span> <span class="nf">hello</span><span class="gl-px-2 gl-rounded-base gl-mx-2 gl-bg-gray-100 gl-cursor-help has-tooltip" title="Content has been trimmed">svg_icon</span></span>)
is_expected.to eq(code)
end
end
it 'returns highlighted ruby code' do
- code = %q{<span id="LC1" class="line" lang="ruby"><span class="k">def</span> <span class="nf">hello</span></span>}
+ code = %q(<span id="LC1" class="line" lang="ruby"><span class="k">def</span> <span class="nf">hello</span></span>)
is_expected.to eq(code)
end
@@ -31,7 +31,7 @@ RSpec.describe Rouge::Formatters::HTMLGitlab, feature_category: :source_code_man
let(:options) { {} }
it 'returns highlighted code without language' do
- code = %q{<span id="LC1" class="line" lang=""><span class="k">def</span> <span class="nf">hello</span></span>}
+ code = %q(<span id="LC1" class="line" lang=""><span class="k">def</span> <span class="nf">hello</span></span>)
is_expected.to eq(code)
end
@@ -41,7 +41,7 @@ RSpec.describe Rouge::Formatters::HTMLGitlab, feature_category: :source_code_man
let(:options) { { tag: lang, line_number: 10 } }
it 'returns highlighted ruby code with correct line number' do
- code = %q{<span id="LC10" class="line" lang="ruby"><span class="k">def</span> <span class="nf">hello</span></span>}
+ code = %q(<span id="LC10" class="line" lang="ruby"><span class="k">def</span> <span class="nf">hello</span></span>)
is_expected.to eq(code)
end
@@ -64,7 +64,7 @@ RSpec.describe Rouge::Formatters::HTMLGitlab, feature_category: :source_code_man
it 'highlights the control characters' do
message = "Potentially unwanted character detected: Unicode BiDi Control"
- is_expected.to include(%{<span class="unicode-bidi has-tooltip" data-toggle="tooltip" title="#{message}">}).exactly(4).times
+ is_expected.to include(%(<span class="unicode-bidi has-tooltip" data-toggle="tooltip" title="#{message}">)).exactly(4).times
end
end
diff --git a/spec/lib/safe_zip/entry_spec.rb b/spec/lib/safe_zip/entry_spec.rb
index 8d49e2bcece..9a068b255dd 100644
--- a/spec/lib/safe_zip/entry_spec.rb
+++ b/spec/lib/safe_zip/entry_spec.rb
@@ -4,8 +4,8 @@ require 'spec_helper'
RSpec.describe SafeZip::Entry do
let(:target_path) { Dir.mktmpdir('safe-zip') }
- let(:directories) { %w(public folder/with/subfolder) }
- let(:files) { %w(public/index.html public/assets/image.png) }
+ let(:directories) { %w[public folder/with/subfolder] }
+ let(:files) { %w[public/index.html public/assets/image.png] }
let(:params) { SafeZip::ExtractParams.new(directories: directories, files: files, to: target_path) }
let(:entry) { described_class.new(zip_archive, zip_entry, params) }
@@ -52,7 +52,7 @@ RSpec.describe SafeZip::Entry do
subject { entry.extract }
context 'when entry does not match the filtered directories' do
- let(:directories) { %w(public folder/with/subfolder) }
+ let(:directories) { %w[public folder/with/subfolder] }
let(:files) { [] }
using RSpec::Parameterized::TableSyntax
@@ -76,7 +76,7 @@ RSpec.describe SafeZip::Entry do
context 'when entry does not match the filtered files' do
let(:directories) { [] }
- let(:files) { %w(public/index.html public/assets/image.png) }
+ let(:files) { %w[public/index.html public/assets/image.png] }
using RSpec::Parameterized::TableSyntax
diff --git a/spec/lib/safe_zip/extract_params_spec.rb b/spec/lib/safe_zip/extract_params_spec.rb
index 0ebfb7430c5..b0d787e09d5 100644
--- a/spec/lib/safe_zip/extract_params_spec.rb
+++ b/spec/lib/safe_zip/extract_params_spec.rb
@@ -6,8 +6,8 @@ RSpec.describe SafeZip::ExtractParams do
let(:target_path) { Dir.mktmpdir("safe-zip") }
let(:real_target_path) { File.realpath(target_path) }
let(:params) { described_class.new(directories: directories, files: files, to: target_path) }
- let(:directories) { %w(public folder/with/subfolder) }
- let(:files) { %w(public/index.html public/assets/image.png) }
+ let(:directories) { %w[public folder/with/subfolder] }
+ let(:files) { %w[public/index.html public/assets/image.png] }
after do
FileUtils.remove_entry_secure(target_path)
diff --git a/spec/lib/safe_zip/extract_spec.rb b/spec/lib/safe_zip/extract_spec.rb
index c727475e271..fa8a922beef 100644
--- a/spec/lib/safe_zip/extract_spec.rb
+++ b/spec/lib/safe_zip/extract_spec.rb
@@ -4,8 +4,8 @@ require 'spec_helper'
RSpec.describe SafeZip::Extract do
let(:target_path) { Dir.mktmpdir('safe-zip') }
- let(:directories) { %w(public) }
- let(:files) { %w(public/index.html) }
+ let(:directories) { %w[public] }
+ let(:files) { %w[public/index.html] }
let(:object) { described_class.new(archive) }
let(:archive) { Rails.root.join('spec', 'fixtures', 'safe_zip', archive_name) }
@@ -47,7 +47,7 @@ RSpec.describe SafeZip::Extract do
end
end
- %w(valid-simple.zip valid-symlinks-first.zip valid-non-writeable.zip).each do |name|
+ %w[valid-simple.zip valid-symlinks-first.zip valid-non-writeable.zip].each do |name|
context "when using #{name} archive" do
let(:archive_name) { name }
@@ -74,7 +74,7 @@ RSpec.describe SafeZip::Extract do
context 'when no matching directories are found' do
let(:archive_name) { 'valid-simple.zip' }
- let(:directories) { %w(non/existing) }
+ let(:directories) { %w[non/existing] }
let(:error_message) { 'No entries extracted' }
subject { object.extract(directories: directories, to: target_path) }
@@ -84,7 +84,7 @@ RSpec.describe SafeZip::Extract do
context 'when no matching files are found' do
let(:archive_name) { 'valid-simple.zip' }
- let(:files) { %w(non/existing) }
+ let(:files) { %w[non/existing] }
let(:error_message) { 'No entries extracted' }
subject { object.extract(files: files, to: target_path) }
diff --git a/spec/lib/sbom/purl_type/converter_spec.rb b/spec/lib/sbom/purl_type/converter_spec.rb
index 2eb35c4d079..d0907bf253f 100644
--- a/spec/lib/sbom/purl_type/converter_spec.rb
+++ b/spec/lib/sbom/purl_type/converter_spec.rb
@@ -22,6 +22,7 @@ RSpec.describe Sbom::PurlType::Converter, feature_category: :dependency_manageme
'nuget' | 'nuget'
'pip' | 'pypi'
'pipenv' | 'pypi'
+ 'poetry' | 'pypi'
'setuptools' | 'pypi'
'Python (python-pkg)' | 'pypi'
'analyzer (gobinary)' | 'golang'
diff --git a/spec/lib/security/ci_configuration/container_scanning_build_action_spec.rb b/spec/lib/security/ci_configuration/container_scanning_build_action_spec.rb
index 5b1db66beb0..af61d9c8261 100644
--- a/spec/lib/security/ci_configuration/container_scanning_build_action_spec.rb
+++ b/spec/lib/security/ci_configuration/container_scanning_build_action_spec.rb
@@ -39,7 +39,7 @@ RSpec.describe Security::CiConfiguration::ContainerScanningBuildAction do
context 'template includes are an array' do
let(:gitlab_ci_content) do
- { "stages" => %w(test security),
+ { "stages" => %w[test security],
"variables" => { "RANDOM" => "make sure this persists" },
"include" => [{ "template" => "existing.yml" }] }
end
@@ -52,7 +52,7 @@ RSpec.describe Security::CiConfiguration::ContainerScanningBuildAction do
context 'template include is not an array' do
let(:gitlab_ci_content) do
- { "stages" => %w(test security),
+ { "stages" => %w[test security],
"variables" => { "RANDOM" => "make sure this persists" },
"include" => { "template" => "existing.yml" } }
end
@@ -91,7 +91,7 @@ RSpec.describe Security::CiConfiguration::ContainerScanningBuildAction do
context 'container_scanning template include are an array' do
let(:gitlab_ci_content) do
- { "stages" => %w(test),
+ { "stages" => %w[test],
"variables" => { "RANDOM" => "make sure this persists" },
"include" => [{ "template" => "Jobs/Container-Scanning.gitlab-ci.yml" }] }
end
@@ -104,7 +104,7 @@ RSpec.describe Security::CiConfiguration::ContainerScanningBuildAction do
context 'container_scanning template include is not an array' do
let(:gitlab_ci_content) do
- { "stages" => %w(test),
+ { "stages" => %w[test],
"variables" => { "RANDOM" => "make sure this persists" },
"include" => { "template" => "Jobs/Container-Scanning.gitlab-ci.yml" } }
end
diff --git a/spec/lib/security/ci_configuration/sast_build_action_spec.rb b/spec/lib/security/ci_configuration/sast_build_action_spec.rb
index 381ea60e7f5..fe504e2b278 100644
--- a/spec/lib/security/ci_configuration/sast_build_action_spec.rb
+++ b/spec/lib/security/ci_configuration/sast_build_action_spec.rb
@@ -218,47 +218,47 @@ RSpec.describe Security::CiConfiguration::SastBuildAction do
end
def existing_gitlab_ci_and_template_array_without_sast
- { "stages" => %w(test security),
+ { "stages" => %w[test security],
"variables" => { "RANDOM" => "make sure this persists", "SECURE_ANALYZERS_PREFIX" => "localhost:5000/analyzers" },
"sast" => { "variables" => { "SEARCH_MAX_DEPTH" => 1 }, "stage" => "security" },
"include" => [{ "template" => "existing.yml" }] }
end
def existing_gitlab_ci_and_single_template_with_sast_and_default_stage
- { "stages" => %w(test),
+ { "stages" => %w[test],
"variables" => { "SECURE_ANALYZERS_PREFIX" => "localhost:5000/analyzers" },
"sast" => { "variables" => { "SEARCH_MAX_DEPTH" => 1 }, "stage" => "test" },
"include" => { "template" => "Security/SAST.gitlab-ci.yml" } }
end
def existing_gitlab_ci_and_single_template_without_sast
- { "stages" => %w(test security),
+ { "stages" => %w[test security],
"variables" => { "RANDOM" => "make sure this persists", "SECURE_ANALYZERS_PREFIX" => "localhost:5000/analyzers" },
"sast" => { "variables" => { "SEARCH_MAX_DEPTH" => 1 }, "stage" => "security" },
"include" => { "template" => "existing.yml" } }
end
def existing_gitlab_ci_with_no_variables
- { "stages" => %w(test security),
+ { "stages" => %w[test security],
"sast" => { "variables" => { "SEARCH_MAX_DEPTH" => 1 }, "stage" => "security" },
"include" => [{ "template" => "Security/SAST.gitlab-ci.yml" }] }
end
def existing_gitlab_ci_with_no_sast_section
- { "stages" => %w(test security),
+ { "stages" => %w[test security],
"variables" => { "RANDOM" => "make sure this persists", "SECURE_ANALYZERS_PREFIX" => "localhost:5000/analyzers" },
"include" => [{ "template" => "Security/SAST.gitlab-ci.yml" }] }
end
def existing_gitlab_ci_with_no_sast_variables
- { "stages" => %w(test security),
+ { "stages" => %w[test security],
"variables" => { "RANDOM" => "make sure this persists", "SECURE_ANALYZERS_PREFIX" => "localhost:5000/analyzers" },
"sast" => { "stage" => "security" },
"include" => [{ "template" => "Security/SAST.gitlab-ci.yml" }] }
end
def existing_gitlab_ci
- { "stages" => %w(test security),
+ { "stages" => %w[test security],
"variables" => { "RANDOM" => "make sure this persists", "SECURE_ANALYZERS_PREFIX" => "bad_prefix" },
"sast" => { "variables" => { "SEARCH_MAX_DEPTH" => 1 }, "stage" => "security" },
"include" => [{ "template" => "Security/SAST.gitlab-ci.yml" }] }
diff --git a/spec/lib/security/ci_configuration/sast_iac_build_action_spec.rb b/spec/lib/security/ci_configuration/sast_iac_build_action_spec.rb
index 7b2a0d22918..fcee34d833b 100644
--- a/spec/lib/security/ci_configuration/sast_iac_build_action_spec.rb
+++ b/spec/lib/security/ci_configuration/sast_iac_build_action_spec.rb
@@ -34,7 +34,7 @@ RSpec.describe Security::CiConfiguration::SastIacBuildAction do
context 'template includes are an array' do
let(:gitlab_ci_content) do
- { "stages" => %w(test security),
+ { "stages" => %w[test security],
"variables" => { "RANDOM" => "make sure this persists" },
"include" => [{ "template" => "existing.yml" }] }
end
@@ -47,7 +47,7 @@ RSpec.describe Security::CiConfiguration::SastIacBuildAction do
context 'template include is not an array' do
let(:gitlab_ci_content) do
- { "stages" => %w(test security),
+ { "stages" => %w[test security],
"variables" => { "RANDOM" => "make sure this persists" },
"include" => { "template" => "existing.yml" } }
end
@@ -80,7 +80,7 @@ RSpec.describe Security::CiConfiguration::SastIacBuildAction do
context 'secret_detection template include are an array' do
let(:gitlab_ci_content) do
- { "stages" => %w(test),
+ { "stages" => %w[test],
"variables" => { "RANDOM" => "make sure this persists" },
"include" => [{ "template" => "Security/SAST-IaC.latest.gitlab-ci.yml" }] }
end
@@ -93,7 +93,7 @@ RSpec.describe Security::CiConfiguration::SastIacBuildAction do
context 'secret_detection template include is not an array' do
let(:gitlab_ci_content) do
- { "stages" => %w(test),
+ { "stages" => %w[test],
"variables" => { "RANDOM" => "make sure this persists" },
"include" => { "template" => "Security/SAST-IaC.latest.gitlab-ci.yml" } }
end
diff --git a/spec/lib/security/ci_configuration/secret_detection_build_action_spec.rb b/spec/lib/security/ci_configuration/secret_detection_build_action_spec.rb
index 4d9860ca4a5..64323ce71f3 100644
--- a/spec/lib/security/ci_configuration/secret_detection_build_action_spec.rb
+++ b/spec/lib/security/ci_configuration/secret_detection_build_action_spec.rb
@@ -33,7 +33,7 @@ RSpec.describe Security::CiConfiguration::SecretDetectionBuildAction do
context 'template includes are an array' do
let(:gitlab_ci_content) do
- { "stages" => %w(test security),
+ { "stages" => %w[test security],
"variables" => { "RANDOM" => "make sure this persists" },
"include" => [{ "template" => "existing.yml" }] }
end
@@ -46,7 +46,7 @@ RSpec.describe Security::CiConfiguration::SecretDetectionBuildAction do
context 'template include is not an array' do
let(:gitlab_ci_content) do
- { "stages" => %w(test security),
+ { "stages" => %w[test security],
"variables" => { "RANDOM" => "make sure this persists" },
"include" => { "template" => "existing.yml" } }
end
@@ -79,7 +79,7 @@ RSpec.describe Security::CiConfiguration::SecretDetectionBuildAction do
context 'secret_detection template include are an array' do
let(:gitlab_ci_content) do
- { "stages" => %w(test),
+ { "stages" => %w[test],
"variables" => { "RANDOM" => "make sure this persists" },
"include" => [{ "template" => "Security/Secret-Detection.gitlab-ci.yml" }] }
end
@@ -92,7 +92,7 @@ RSpec.describe Security::CiConfiguration::SecretDetectionBuildAction do
context 'secret_detection template include is not an array' do
let(:gitlab_ci_content) do
- { "stages" => %w(test),
+ { "stages" => %w[test],
"variables" => { "RANDOM" => "make sure this persists" },
"include" => { "template" => "Security/Secret-Detection.gitlab-ci.yml" } }
end
diff --git a/spec/lib/sidebars/explore/menus/catalog_menu_spec.rb b/spec/lib/sidebars/explore/menus/catalog_menu_spec.rb
new file mode 100644
index 00000000000..2c4c4c48eae
--- /dev/null
+++ b/spec/lib/sidebars/explore/menus/catalog_menu_spec.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Sidebars::Explore::Menus::CatalogMenu, feature_category: :navigation do
+ let_it_be(:current_user) { build(:user) }
+ let_it_be(:user) { build(:user) }
+
+ let(:context) { Sidebars::Context.new(current_user: current_user, container: user) }
+
+ subject { described_class.new(context) }
+
+ context 'when `global_ci_catalog` is enabled`' do
+ it 'renders' do
+ expect(subject.render?).to be(true)
+ end
+
+ it 'renders the correct link' do
+ expect(subject.link).to match "explore/catalog"
+ end
+
+ it 'renders the correct title' do
+ expect(subject.title).to eq "CI/CD Catalog"
+ end
+
+ it 'renders the correct icon' do
+ expect(subject.sprite_icon).to eq "catalog-checkmark"
+ end
+ end
+
+ context 'when `global_ci_catalog` FF is disabled' do
+ before do
+ stub_feature_flags(global_ci_catalog: false)
+ end
+
+ it 'does not render' do
+ expect(subject.render?).to be(false)
+ end
+ end
+end
diff --git a/spec/lib/sidebars/menu_spec.rb b/spec/lib/sidebars/menu_spec.rb
index e59a8cd2163..aa3b754f17e 100644
--- a/spec/lib/sidebars/menu_spec.rb
+++ b/spec/lib/sidebars/menu_spec.rb
@@ -10,7 +10,7 @@ RSpec.describe Sidebars::Menu, feature_category: :navigation do
describe '#all_active_routes' do
it 'gathers all active routes of items and the current menu' do
- menu.add_item(Sidebars::MenuItem.new(title: 'foo1', link: 'foo1', active_routes: { path: %w(bar test) }))
+ menu.add_item(Sidebars::MenuItem.new(title: 'foo1', link: 'foo1', active_routes: { path: %w[bar test] }))
menu.add_item(Sidebars::MenuItem.new(title: 'foo2', link: 'foo2', active_routes: { controller: 'fooc' }))
menu.add_item(Sidebars::MenuItem.new(title: 'foo3', link: 'foo3', active_routes: { controller: 'barc' }))
menu.add_item(nil_menu_item)
@@ -18,7 +18,7 @@ RSpec.describe Sidebars::Menu, feature_category: :navigation do
allow(menu).to receive(:active_routes).and_return({ path: 'foo' })
expect(menu).to receive(:renderable_items).and_call_original
- expect(menu.all_active_routes).to eq({ path: %w(foo bar test), controller: %w(fooc barc) })
+ expect(menu.all_active_routes).to eq({ path: %w[foo bar test], controller: %w[fooc barc] })
end
end
@@ -53,6 +53,7 @@ RSpec.describe Sidebars::Menu, feature_category: :navigation do
{
title: "Title",
icon: nil,
+ id: 'menu',
avatar: nil,
avatar_shape: 'rect',
entity_id: nil,
@@ -94,6 +95,7 @@ RSpec.describe Sidebars::Menu, feature_category: :navigation do
{
title: "Title",
icon: nil,
+ id: 'menu',
avatar: nil,
avatar_shape: 'rect',
entity_id: nil,
diff --git a/spec/lib/sidebars/organizations/menus/manage_menu_spec.rb b/spec/lib/sidebars/organizations/menus/manage_menu_spec.rb
index 08fc352a6cd..87346176a4c 100644
--- a/spec/lib/sidebars/organizations/menus/manage_menu_spec.rb
+++ b/spec/lib/sidebars/organizations/menus/manage_menu_spec.rb
@@ -24,5 +24,11 @@ RSpec.describe Sidebars::Organizations::Menus::ManageMenu, feature_category: :na
it { is_expected.not_to be_nil }
end
+
+ describe 'Users' do
+ let(:item_id) { :organization_users }
+
+ it { is_expected.not_to be_nil }
+ end
end
end
diff --git a/spec/lib/sidebars/projects/menus/scope_menu_spec.rb b/spec/lib/sidebars/projects/menus/scope_menu_spec.rb
index 1c2d159950a..108a98e28a4 100644
--- a/spec/lib/sidebars/projects/menus/scope_menu_spec.rb
+++ b/spec/lib/sidebars/projects/menus/scope_menu_spec.rb
@@ -23,7 +23,7 @@ RSpec.describe Sidebars::Projects::Menus::ScopeMenu, feature_category: :navigati
describe '#container_html_options' do
subject { described_class.new(context).container_html_options }
- specify { is_expected.to match(hash_including(class: 'shortcuts-project rspec-project-link')) }
+ specify { is_expected.to match(hash_including(class: 'shortcuts-project')) }
end
describe '#extra_nav_link_html_options' do
diff --git a/spec/lib/sidebars/projects/menus/security_compliance_menu_spec.rb b/spec/lib/sidebars/projects/menus/security_compliance_menu_spec.rb
index 4b4706bd311..c7c0586c2f1 100644
--- a/spec/lib/sidebars/projects/menus/security_compliance_menu_spec.rb
+++ b/spec/lib/sidebars/projects/menus/security_compliance_menu_spec.rb
@@ -21,9 +21,12 @@ RSpec.describe Sidebars::Projects::Menus::SecurityComplianceMenu do
context 'when user is authenticated' do
context 'when the Security and Compliance is disabled' do
+ let_it_be(:project) { create(:project, :security_and_compliance_disabled) }
+
before do
allow(Ability).to receive(:allowed?).with(user, :access_security_and_compliance, project).and_return(false)
allow(Ability).to receive(:allowed?).with(user, :read_security_resource, project).and_return(false)
+ allow(project).to receive(:security_and_compliance_enabled?).and_return(false)
end
it { is_expected.to be_falsey }
diff --git a/spec/lib/sidebars/projects/menus/settings_menu_spec.rb b/spec/lib/sidebars/projects/menus/settings_menu_spec.rb
index 81ca9670ac6..605cec8be5e 100644
--- a/spec/lib/sidebars/projects/menus/settings_menu_spec.rb
+++ b/spec/lib/sidebars/projects/menus/settings_menu_spec.rb
@@ -59,18 +59,6 @@ RSpec.describe Sidebars::Projects::Menus::SettingsMenu, feature_category: :navig
let(:item_id) { :access_tokens }
it_behaves_like 'access rights checks'
-
- describe 'when the user is not an admin but has manage_resource_access_tokens' do
- before do
- allow(Ability).to receive(:allowed?).and_call_original
- allow(Ability).to receive(:allowed?).with(user, :admin_project, project).and_return(false)
- allow(Ability).to receive(:allowed?).with(user, :manage_resource_access_tokens, project).and_return(true)
- end
-
- it 'includes access token menu item' do
- expect(subject.title).to eql('Access Tokens')
- end
- end
end
describe 'Repository' do
diff --git a/spec/lib/sidebars/projects/super_sidebar_menus/monitor_menu_spec.rb b/spec/lib/sidebars/projects/super_sidebar_menus/monitor_menu_spec.rb
index e5c5204e0b4..3f8a146f040 100644
--- a/spec/lib/sidebars/projects/super_sidebar_menus/monitor_menu_spec.rb
+++ b/spec/lib/sidebars/projects/super_sidebar_menus/monitor_menu_spec.rb
@@ -16,6 +16,7 @@ RSpec.describe Sidebars::Projects::SuperSidebarMenus::MonitorMenu, feature_categ
expect(items.map(&:class).uniq).to eq([Sidebars::NilMenuItem])
expect(items.map(&:item_id)).to eq([
:tracing,
+ :metrics,
:error_tracking,
:alert_management,
:incidents,
diff --git a/spec/lib/sidebars/user_settings/menus/comment_templates_menu_spec.rb b/spec/lib/sidebars/user_settings/menus/comment_templates_menu_spec.rb
index 37a383cfd9d..9a093efe6ba 100644
--- a/spec/lib/sidebars/user_settings/menus/comment_templates_menu_spec.rb
+++ b/spec/lib/sidebars/user_settings/menus/comment_templates_menu_spec.rb
@@ -15,10 +15,6 @@ RSpec.describe Sidebars::UserSettings::Menus::CommentTemplatesMenu, feature_cate
let_it_be(:user) { build(:user) }
context 'when comment templates are enabled' do
- before do
- allow(subject).to receive(:saved_replies_enabled?).and_return(true)
- end
-
context 'when user is logged in' do
let(:context) { Sidebars::Context.new(current_user: user, container: nil) }
@@ -37,29 +33,5 @@ RSpec.describe Sidebars::UserSettings::Menus::CommentTemplatesMenu, feature_cate
end
end
end
-
- context 'when comment templates are disabled' do
- before do
- allow(subject).to receive(:saved_replies_enabled?).and_return(false)
- end
-
- context 'when user is logged in' do
- let(:context) { Sidebars::Context.new(current_user: user, container: nil) }
-
- it 'renders' do
- expect(subject.render?).to be false
- end
- end
-
- context 'when user is not logged in' do
- let(:context) { Sidebars::Context.new(current_user: nil, container: nil) }
-
- subject { described_class.new(context) }
-
- it 'does not render' do
- expect(subject.render?).to be false
- end
- end
- end
end
end
diff --git a/spec/lib/system_check/orphans/namespace_check_spec.rb b/spec/lib/system_check/orphans/namespace_check_spec.rb
index e764c2313cd..3964068b20c 100644
--- a/spec/lib/system_check/orphans/namespace_check_spec.rb
+++ b/spec/lib/system_check/orphans/namespace_check_spec.rb
@@ -12,10 +12,10 @@ RSpec.describe SystemCheck::Orphans::NamespaceCheck, :silence_stdout do
describe '#multi_check' do
context 'all orphans' do
- let(:disk_namespaces) { %w(/repos/orphan1 /repos/orphan2 repos/@hashed) }
+ let(:disk_namespaces) { %w[/repos/orphan1 /repos/orphan2 repos/@hashed] }
it 'prints list of all orphaned namespaces except @hashed' do
- expect_list_of_orphans(%w(orphan1 orphan2))
+ expect_list_of_orphans(%w[orphan1 orphan2])
subject.multi_check
end
@@ -23,10 +23,10 @@ RSpec.describe SystemCheck::Orphans::NamespaceCheck, :silence_stdout do
context 'few orphans with existing namespace' do
let!(:first_level) { create(:group, path: 'my-namespace') }
- let(:disk_namespaces) { %w(/repos/orphan1 /repos/orphan2 /repos/my-namespace /repos/@hashed) }
+ let(:disk_namespaces) { %w[/repos/orphan1 /repos/orphan2 /repos/my-namespace /repos/@hashed] }
it 'prints list of orphaned namespaces' do
- expect_list_of_orphans(%w(orphan1 orphan2))
+ expect_list_of_orphans(%w[orphan1 orphan2])
subject.multi_check
end
@@ -35,17 +35,17 @@ RSpec.describe SystemCheck::Orphans::NamespaceCheck, :silence_stdout do
context 'few orphans with existing namespace and parents with same name as orphans' do
let!(:first_level) { create(:group, path: 'my-namespace') }
let!(:second_level) { create(:group, path: 'second-level', parent: first_level) }
- let(:disk_namespaces) { %w(/repos/orphan1 /repos/orphan2 /repos/my-namespace /repos/second-level /repos/@hashed) }
+ let(:disk_namespaces) { %w[/repos/orphan1 /repos/orphan2 /repos/my-namespace /repos/second-level /repos/@hashed] }
it 'prints list of orphaned namespaces ignoring parents with same namespace as orphans' do
- expect_list_of_orphans(%w(orphan1 orphan2 second-level))
+ expect_list_of_orphans(%w[orphan1 orphan2 second-level])
subject.multi_check
end
end
context 'no orphans' do
- let(:disk_namespaces) { %w(@hashed) }
+ let(:disk_namespaces) { %w[@hashed] }
it 'prints an empty list ignoring @hashed' do
expect_list_of_orphans([])
diff --git a/spec/lib/system_check/orphans/repository_check_spec.rb b/spec/lib/system_check/orphans/repository_check_spec.rb
index 91b48969cc1..0504e133ab9 100644
--- a/spec/lib/system_check/orphans/repository_check_spec.rb
+++ b/spec/lib/system_check/orphans/repository_check_spec.rb
@@ -13,11 +13,11 @@ RSpec.describe SystemCheck::Orphans::RepositoryCheck, :silence_stdout do
describe '#multi_check' do
context 'all orphans' do
- let(:disk_namespaces) { %w(/repos/orphan1 /repos/orphan2 repos/@hashed) }
- let(:disk_repositories) { %w(repo1.git repo2.git) }
+ let(:disk_namespaces) { %w[/repos/orphan1 /repos/orphan2 repos/@hashed] }
+ let(:disk_repositories) { %w[repo1.git repo2.git] }
it 'prints list of all orphaned namespaces except @hashed' do
- expect_list_of_orphans(%w(orphan1/repo1.git orphan1/repo2.git orphan2/repo1.git orphan2/repo2.git))
+ expect_list_of_orphans(%w[orphan1/repo1.git orphan1/repo2.git orphan2/repo1.git orphan2/repo2.git])
subject.multi_check
end
@@ -26,11 +26,11 @@ RSpec.describe SystemCheck::Orphans::RepositoryCheck, :silence_stdout do
context 'few orphans with existing namespace' do
let!(:first_level) { create(:group, path: 'my-namespace') }
let!(:project) { create(:project, path: 'repo', namespace: first_level) }
- let(:disk_namespaces) { %w(/repos/orphan1 /repos/orphan2 /repos/my-namespace /repos/@hashed) }
- let(:disk_repositories) { %w(repo.git) }
+ let(:disk_namespaces) { %w[/repos/orphan1 /repos/orphan2 /repos/my-namespace /repos/@hashed] }
+ let(:disk_repositories) { %w[repo.git] }
it 'prints list of orphaned namespaces' do
- expect_list_of_orphans(%w(orphan1/repo.git orphan2/repo.git))
+ expect_list_of_orphans(%w[orphan1/repo.git orphan2/repo.git])
subject.multi_check
end
@@ -40,19 +40,19 @@ RSpec.describe SystemCheck::Orphans::RepositoryCheck, :silence_stdout do
let!(:first_level) { create(:group, path: 'my-namespace') }
let!(:second_level) { create(:group, path: 'second-level', parent: first_level) }
let!(:project) { create(:project, path: 'repo', namespace: first_level) }
- let(:disk_namespaces) { %w(/repos/orphan1 /repos/orphan2 /repos/my-namespace /repos/second-level /repos/@hashed) }
- let(:disk_repositories) { %w(repo.git) }
+ let(:disk_namespaces) { %w[/repos/orphan1 /repos/orphan2 /repos/my-namespace /repos/second-level /repos/@hashed] }
+ let(:disk_repositories) { %w[repo.git] }
it 'prints list of orphaned namespaces ignoring parents with same namespace as orphans' do
- expect_list_of_orphans(%w(orphan1/repo.git orphan2/repo.git second-level/repo.git))
+ expect_list_of_orphans(%w[orphan1/repo.git orphan2/repo.git second-level/repo.git])
subject.multi_check
end
end
context 'no orphans' do
- let(:disk_namespaces) { %w(@hashed) }
- let(:disk_repositories) { %w(repo.git) }
+ let(:disk_namespaces) { %w[@hashed] }
+ let(:disk_repositories) { %w[repo.git] }
it 'prints an empty list ignoring @hashed' do
expect_list_of_orphans([])
diff --git a/spec/lib/system_check/sidekiq_check_spec.rb b/spec/lib/system_check/sidekiq_check_spec.rb
index ff4eece8f7c..efd5414294a 100644
--- a/spec/lib/system_check/sidekiq_check_spec.rb
+++ b/spec/lib/system_check/sidekiq_check_spec.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe SystemCheck::SidekiqCheck do
describe '#multi_check' do
def stub_ps_output(output)
- allow(Gitlab::Popen).to receive(:popen).with(%w(ps uxww)).and_return([output, nil])
+ allow(Gitlab::Popen).to receive(:popen).with(%w[ps uxww]).and_return([output, nil])
end
def expect_check_output(matcher)
diff --git a/spec/lib/unnested_in_filters/dsl_spec.rb b/spec/lib/unnested_in_filters/dsl_spec.rb
index bce4c88f94c..9f1552b02ec 100644
--- a/spec/lib/unnested_in_filters/dsl_spec.rb
+++ b/spec/lib/unnested_in_filters/dsl_spec.rb
@@ -12,7 +12,7 @@ RSpec.describe UnnestedInFilters::Dsl do
end
describe '#exists?' do
- let(:states) { %w(active banned) }
+ let(:states) { %w[active banned] }
subject { test_model.where(state: states).use_unnested_filters.exists? }
diff --git a/spec/lib/unnested_in_filters/rewriter_spec.rb b/spec/lib/unnested_in_filters/rewriter_spec.rb
index ea561c42993..945a50ce2e8 100644
--- a/spec/lib/unnested_in_filters/rewriter_spec.rb
+++ b/spec/lib/unnested_in_filters/rewriter_spec.rb
@@ -26,7 +26,7 @@ RSpec.describe UnnestedInFilters::Rewriter do
context 'when the given relation has an `IN` predicate' do
context 'when there is no index coverage for the used columns' do
- let(:relation) { User.where(username: %w(user_1 user_2), state: :active) }
+ let(:relation) { User.where(username: %w[user_1 user_2], state: :active) }
it { is_expected.to be_falsey }
end
@@ -37,7 +37,7 @@ RSpec.describe UnnestedInFilters::Rewriter do
it { is_expected.to be_truthy }
context 'when there is an ordering' do
- let(:relation) { User.where(state: %w(active blocked banned)).order(order).limit(2) }
+ let(:relation) { User.where(state: %w[active blocked banned]).order(order).limit(2) }
context 'when the order is an Arel node' do
let(:order) { { user_type: :desc } }
@@ -67,7 +67,7 @@ RSpec.describe UnnestedInFilters::Rewriter do
describe '#rewrite' do
let(:recorded_queries) { ActiveRecord::QueryRecorder.new { rewriter.rewrite.load } }
- let(:relation) { User.where(state: :active, user_type: %i(support_bot alert_bot)).limit(2) }
+ let(:relation) { User.where(state: :active, user_type: %i[support_bot alert_bot]).limit(2) }
let(:users_select) { 'SELECT "users".*' }
let(:users_select_with_ignored_columns) { 'SELECT ("users"."\w+", )+("users"."\w+")' }
@@ -101,7 +101,7 @@ RSpec.describe UnnestedInFilters::Rewriter do
end
context 'when the relation has a subquery' do
- let(:relation) { User.where(state: User.select(:state), user_type: %i(support_bot alert_bot)).limit(1) }
+ let(:relation) { User.where(state: User.select(:state), user_type: %i[support_bot alert_bot]).limit(1) }
let(:users_unnest) do
'FROM
@@ -127,7 +127,7 @@ RSpec.describe UnnestedInFilters::Rewriter do
end
context 'when there is an order' do
- let(:relation) { User.where(state: %w(active blocked banned)).order(order).limit(2) }
+ let(:relation) { User.where(state: %w[active blocked banned]).order(order).limit(2) }
let(:users_unnest) do
'FROM
@@ -177,7 +177,7 @@ RSpec.describe UnnestedInFilters::Rewriter do
end
context 'when the combined attributes include the primary key' do
- let(:relation) { User.where(user_type: %i(support_bot alert_bot)).order(id: :desc).limit(2) }
+ let(:relation) { User.where(user_type: %i[support_bot alert_bot]).order(id: :desc).limit(2) }
let(:users_where) do
'FROM
diff --git a/spec/mailers/emails/pages_domains_spec.rb b/spec/mailers/emails/pages_domains_spec.rb
index ff446b83412..88e5d60d6d3 100644
--- a/spec/mailers/emails/pages_domains_spec.rb
+++ b/spec/mailers/emails/pages_domains_spec.rb
@@ -33,7 +33,7 @@ RSpec.describe Emails::PagesDomains do
it 'has the expected content' do
is_expected.to have_body_text domain.url
- is_expected.to have_body_text help_page_url('user/project/pages/custom_domains_ssl_tls_certification/index.md', anchor: link_anchor)
+ is_expected.to have_body_text help_page_url('user/project/pages/custom_domains_ssl_tls_certification/index', anchor: link_anchor)
end
end
@@ -112,7 +112,7 @@ RSpec.describe Emails::PagesDomains do
it 'says that we failed to obtain certificate' do
is_expected.to have_body_text "Something went wrong while obtaining the Let's Encrypt certificate."
- is_expected.to have_body_text help_page_url('user/project/pages/custom_domains_ssl_tls_certification/lets_encrypt_integration.md', anchor: 'troubleshooting')
+ is_expected.to have_body_text help_page_url('user/project/pages/custom_domains_ssl_tls_certification/lets_encrypt_integration', anchor: 'troubleshooting')
end
end
end
diff --git a/spec/metrics_server/metrics_server_spec.rb b/spec/metrics_server/metrics_server_spec.rb
index baf15a773b1..1d53ba194b5 100644
--- a/spec/metrics_server/metrics_server_spec.rb
+++ b/spec/metrics_server/metrics_server_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
require_relative '../../metrics_server/metrics_server'
-RSpec.describe MetricsServer, feature_category: :application_performance do # rubocop:disable RSpec/FilePath
+RSpec.describe MetricsServer, feature_category: :cloud_connector do
let(:prometheus_config) { ::Prometheus::Client.configuration }
let(:metrics_dir) { Dir.mktmpdir }
@@ -16,7 +16,7 @@ RSpec.describe MetricsServer, feature_category: :application_performance do # ru
before do
# Make sure we never actually spawn any new processes in a unit test.
- %i(spawn fork detach).each { |m| allow(Process).to receive(m) }
+ %i[spawn fork detach].each { |m| allow(Process).to receive(m) }
# We do not want this to have knock-on effects on the test process.
allow(Gitlab::ProcessManagement).to receive(:modify_signals)
@@ -33,7 +33,7 @@ RSpec.describe MetricsServer, feature_category: :application_performance do # ru
FileUtils.rm_rf(metrics_dir, secure: true)
end
- %w(puma sidekiq).each do |target|
+ %w[puma sidekiq].each do |target|
context "when targeting #{target}" do
describe '.fork' do
context 'when in parent process' do
diff --git a/spec/migrations/20230929155123_migrate_disable_merge_trains_value_spec.rb b/spec/migrations/20230929155123_migrate_disable_merge_trains_value_spec.rb
new file mode 100644
index 00000000000..ee011687bbb
--- /dev/null
+++ b/spec/migrations/20230929155123_migrate_disable_merge_trains_value_spec.rb
@@ -0,0 +1,83 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration! 'migrate_disable_merge_trains_value'
+
+RSpec.describe MigrateDisableMergeTrainsValue, schema: 20230929155123, feature_category: :continuous_integration do
+ let!(:feature_gates) { table(:feature_gates) }
+ let!(:projects) { table(:projects) }
+ let!(:project_ci_cd_settings) { table(:project_ci_cd_settings) }
+ let!(:namespace1) { table(:namespaces).create!(name: 'name1', path: 'path1') }
+ let!(:namespace2) { table(:namespaces).create!(name: 'name2', path: 'path2') }
+
+ let!(:project_with_flag_on) do
+ projects
+ .create!(
+ name: "project",
+ path: "project",
+ namespace_id: namespace1.id,
+ project_namespace_id: namespace1.id
+ )
+ end
+
+ let!(:project_with_flag_off) do
+ projects
+ .create!(
+ name: "project2",
+ path: "project2",
+ namespace_id: namespace2.id,
+ project_namespace_id: namespace2.id
+ )
+ end
+
+ let!(:settings_flag_on) do
+ project_ci_cd_settings.create!(
+ merge_trains_enabled: true,
+ project_id: project_with_flag_on.id
+ )
+ end
+
+ let!(:settings_flag_off) do
+ project_ci_cd_settings.create!(
+ merge_trains_enabled: true,
+ project_id: project_with_flag_off.id
+ )
+ end
+
+ let!(:migration) { described_class.new }
+
+ before do
+ # Enable the feature flag
+ feature_gates.create!(
+ feature_key: 'disable_merge_trains',
+ key: 'actors',
+ value: "Project:#{project_with_flag_on.id}"
+ )
+
+ migration.up
+ end
+
+ describe '#up' do
+ it 'migrates the flag value into the setting value' do
+ expect(
+ settings_flag_on.reload.merge_trains_enabled
+ ).to eq(false)
+ expect(
+ settings_flag_off.reload.merge_trains_enabled
+ ).to eq(true)
+ end
+ end
+
+ describe '#down' do
+ it 'reverts the migration' do
+ migration.down
+
+ expect(
+ settings_flag_on.reload.merge_trains_enabled
+ ).to eq(true)
+ expect(
+ settings_flag_off.reload.merge_trains_enabled
+ ).to eq(true)
+ end
+ end
+end
diff --git a/spec/migrations/20231003045342_migrate_sidekiq_namespaced_jobs_spec.rb b/spec/migrations/20231003045342_migrate_sidekiq_namespaced_jobs_spec.rb
new file mode 100644
index 00000000000..9e170ff33a4
--- /dev/null
+++ b/spec/migrations/20231003045342_migrate_sidekiq_namespaced_jobs_spec.rb
@@ -0,0 +1,67 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe MigrateSidekiqNamespacedJobs, :migration, feature_category: :scalability do
+ before do
+ q1 = instance_double(Sidekiq::Queue, name: 'q1')
+ q2 = instance_double(Sidekiq::Queue, name: 'q2')
+ allow(Sidekiq::Queue).to receive(:all).and_return([q1, q2])
+
+ Gitlab::Redis::Queues.with do |redis|
+ (1..1000).each do |i|
+ redis.zadd('resque:gitlab:schedule', [i, i])
+ redis.zadd('resque:gitlab:retry', [i, i])
+ redis.zadd('resque:gitlab:dead', [i, i])
+ end
+
+ Sidekiq::Queue.all.each do |queue|
+ (1..1000).each do |i|
+ redis.lpush("resque:gitlab:queue:#{queue.name}", i)
+ end
+ end
+ end
+ end
+
+ after do
+ Gitlab::Redis::Queues.with(&:flushdb)
+ end
+
+ it "does not creates default organization if needed" do
+ reversible_migration do |migration|
+ migration.before -> {
+ Gitlab::Redis::Queues.with do |redis|
+ expect(redis.zcard('resque:gitlab:schedule')).to eq(1000)
+ expect(redis.zcard('resque:gitlab:retry')).to eq(1000)
+ expect(redis.zcard('resque:gitlab:dead')).to eq(1000)
+ expect(redis.zcard('schedule')).to eq(0)
+ expect(redis.zcard('retry')).to eq(0)
+ expect(redis.zcard('dead')).to eq(0)
+
+ Sidekiq::Queue.all.each do |queue|
+ expect(redis.llen("resque:gitlab:queue:#{queue.name}")).to eq(1000)
+ expect(redis.llen("queue:#{queue.name}")).to eq(0)
+ end
+ end
+ }
+
+ migration.after -> {
+ # no namespaced keys
+ Gitlab::Redis::Queues.with do |redis|
+ expect(redis.zcard('resque:gitlab:schedule')).to eq(0)
+ expect(redis.zcard('resque:gitlab:retry')).to eq(0)
+ expect(redis.zcard('resque:gitlab:dead')).to eq(0)
+ expect(redis.zcard('schedule')).to eq(1000)
+ expect(redis.zcard('retry')).to eq(1000)
+ expect(redis.zcard('dead')).to eq(1000)
+
+ Sidekiq::Queue.all.each do |queue|
+ expect(redis.llen("resque:gitlab:queue:#{queue.name}")).to eq(0)
+ expect(redis.llen("queue:#{queue.name}")).to eq(1000)
+ end
+ end
+ }
+ end
+ end
+end
diff --git a/spec/migrations/20231016001000_fix_design_user_mentions_design_id_note_id_index_for_self_managed_spec.rb b/spec/migrations/20231016001000_fix_design_user_mentions_design_id_note_id_index_for_self_managed_spec.rb
new file mode 100644
index 00000000000..8d890bb7cac
--- /dev/null
+++ b/spec/migrations/20231016001000_fix_design_user_mentions_design_id_note_id_index_for_self_managed_spec.rb
@@ -0,0 +1,114 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+require_migration!
+
+RSpec.describe FixDesignUserMentionsDesignIdNoteIdIndexForSelfManaged, feature_category: :database do
+ let(:connection) { described_class.new.connection }
+ let(:design_user_mentions) { table(:design_user_mentions) }
+
+ shared_examples 'index `design_user_mentions_on_design_id_and_note_id_unique_index` already exists' do
+ it 'does not swap the columns' do
+ disable_migrations_output do
+ reversible_migration do |migration|
+ migration.before -> {
+ index = connection.indexes(:design_user_mentions).find do |i|
+ i.name == 'design_user_mentions_on_design_id_and_note_id_unique_index'
+ end
+ expect(index.columns).to eq(%w[design_id note_id])
+ }
+
+ migration.after -> {
+ index = connection.indexes(:design_user_mentions).find do |i|
+ i.name == 'design_user_mentions_on_design_id_and_note_id_unique_index'
+ end
+ expect(index.columns).to eq(%w[design_id note_id])
+ }
+ end
+ end
+ end
+ end
+
+ describe '#up' do
+ before do
+ # rubocop:disable RSpec/AnyInstanceOf
+ allow_any_instance_of(described_class).to(
+ receive(:com_or_dev_or_test_but_not_jh?).and_return(com_or_dev_or_test_but_not_jh?)
+ )
+ # rubocop:enable RSpec/AnyInstanceOf
+ end
+
+ context 'when GitLab.com, dev, or test' do
+ let(:com_or_dev_or_test_but_not_jh?) { true }
+
+ it_behaves_like 'index `design_user_mentions_on_design_id_and_note_id_unique_index` already exists'
+ end
+
+ context 'when self-managed instance' do
+ let(:com_or_dev_or_test_but_not_jh?) { false }
+
+ context "when index does not exist" do
+ before do
+ connection.execute('DROP INDEX IF EXISTS design_user_mentions_on_design_id_and_note_id_unique_index')
+ end
+
+ after do
+ connection.execute('CREATE UNIQUE INDEX IF NOT EXISTS
+ design_user_mentions_on_design_id_and_note_id_unique_index
+ ON design_user_mentions (design_id, note_id)')
+ end
+
+ it 'creates the index' do
+ disable_migrations_output { migrate! }
+
+ index = connection.indexes(:design_user_mentions).find do |i|
+ i.name == 'design_user_mentions_on_design_id_and_note_id_unique_index'
+ end
+
+ expect(index.columns).to eq(%w[design_id note_id])
+ end
+ end
+
+ context "when index does exist" do
+ it_behaves_like 'index `design_user_mentions_on_design_id_and_note_id_unique_index` already exists'
+ end
+
+ context "when index does exists on the int4 column" do
+ before do
+ connection.execute('DROP INDEX IF EXISTS design_user_mentions_on_design_id_and_note_id_unique_index')
+ connection.execute(
+ 'ALTER TABLE design_user_mentions ADD COLUMN IF NOT EXISTS note_id_convert_to_bigint integer'
+ )
+ connection.execute('CREATE UNIQUE INDEX
+ design_user_mentions_on_design_id_and_note_id_unique_index
+ ON design_user_mentions (design_id, note_id_convert_to_bigint)')
+ end
+
+ after do
+ connection.execute('DROP INDEX IF EXISTS design_user_mentions_on_design_id_and_note_id_unique_index')
+ connection.execute('ALTER TABLE design_user_mentions DROP COLUMN IF EXISTS note_id_convert_to_bigint')
+ connection.execute('CREATE UNIQUE INDEX
+ design_user_mentions_on_design_id_and_note_id_unique_index
+ ON design_user_mentions (design_id, note_id)')
+ end
+
+ it 'creates the index on the int8 column' do
+ index = connection.indexes(:design_user_mentions).find do |i|
+ i.name == 'design_user_mentions_on_design_id_and_note_id_unique_index'
+ end
+
+ expect(index.columns).to eq(%w[design_id note_id_convert_to_bigint])
+
+ disable_migrations_output { migrate! }
+
+ index = connection.indexes(:design_user_mentions).find do |i|
+ i.name == 'design_user_mentions_on_design_id_and_note_id_unique_index'
+ end
+
+ expect(index.columns).to eq(%w[design_id note_id])
+ end
+ end
+ end
+ end
+end
diff --git a/spec/migrations/20231016173129_queue_delete_invalid_protected_branch_merge_access_levels_spec.rb b/spec/migrations/20231016173129_queue_delete_invalid_protected_branch_merge_access_levels_spec.rb
new file mode 100644
index 00000000000..292fdca026f
--- /dev/null
+++ b/spec/migrations/20231016173129_queue_delete_invalid_protected_branch_merge_access_levels_spec.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe QueueDeleteInvalidProtectedBranchMergeAccessLevels, feature_category: :source_code_management do
+ let!(:batched_migration) { described_class::MIGRATION }
+
+ it 'schedules a new batched migration' do
+ reversible_migration do |migration|
+ migration.before -> {
+ expect(batched_migration).not_to have_scheduled_batched_migration
+ }
+
+ migration.after -> {
+ expect(batched_migration).to have_scheduled_batched_migration(
+ table_name: :protected_branch_merge_access_levels,
+ column_name: :id,
+ interval: described_class::DELAY_INTERVAL,
+ batch_size: described_class::BATCH_SIZE,
+ sub_batch_size: described_class::SUB_BATCH_SIZE
+ )
+ }
+ end
+ end
+end
diff --git a/spec/migrations/20231016194927_queue_delete_invalid_protected_branch_push_access_levels_spec.rb b/spec/migrations/20231016194927_queue_delete_invalid_protected_branch_push_access_levels_spec.rb
new file mode 100644
index 00000000000..db42bb05f15
--- /dev/null
+++ b/spec/migrations/20231016194927_queue_delete_invalid_protected_branch_push_access_levels_spec.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe QueueDeleteInvalidProtectedBranchPushAccessLevels, feature_category: :source_code_management do
+ let!(:batched_migration) { described_class::MIGRATION }
+
+ it 'schedules a new batched migration' do
+ reversible_migration do |migration|
+ migration.before -> {
+ expect(batched_migration).not_to have_scheduled_batched_migration
+ }
+
+ migration.after -> {
+ expect(batched_migration).to have_scheduled_batched_migration(
+ table_name: :protected_branch_push_access_levels,
+ column_name: :id,
+ interval: described_class::DELAY_INTERVAL,
+ batch_size: described_class::BATCH_SIZE,
+ sub_batch_size: described_class::SUB_BATCH_SIZE
+ )
+ }
+ end
+ end
+end
diff --git a/spec/migrations/20231016194943_queue_delete_invalid_protected_tag_create_access_levels_spec.rb b/spec/migrations/20231016194943_queue_delete_invalid_protected_tag_create_access_levels_spec.rb
new file mode 100644
index 00000000000..4acc46a65c5
--- /dev/null
+++ b/spec/migrations/20231016194943_queue_delete_invalid_protected_tag_create_access_levels_spec.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe QueueDeleteInvalidProtectedTagCreateAccessLevels, feature_category: :source_code_management do
+ let!(:batched_migration) { described_class::MIGRATION }
+
+ it 'schedules a new batched migration' do
+ reversible_migration do |migration|
+ migration.before -> {
+ expect(batched_migration).not_to have_scheduled_batched_migration
+ }
+
+ migration.after -> {
+ expect(batched_migration).to have_scheduled_batched_migration(
+ table_name: :protected_tag_create_access_levels,
+ column_name: :id,
+ interval: described_class::DELAY_INTERVAL,
+ batch_size: described_class::BATCH_SIZE,
+ sub_batch_size: described_class::SUB_BATCH_SIZE
+ )
+ }
+ end
+ end
+end
diff --git a/spec/migrations/20231019003052_swap_columns_for_ci_pipelines_pipeline_id_bigint_v2_spec.rb b/spec/migrations/20231019003052_swap_columns_for_ci_pipelines_pipeline_id_bigint_v2_spec.rb
new file mode 100644
index 00000000000..9fc07a0ac76
--- /dev/null
+++ b/spec/migrations/20231019003052_swap_columns_for_ci_pipelines_pipeline_id_bigint_v2_spec.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe SwapColumnsForCiPipelinesPipelineIdBigintV2, feature_category: :continuous_integration do
+ context 'when auto_canceled_by_id sql type is integer' do
+ before do
+ active_record_base.connection.execute(<<~SQL)
+ ALTER TABLE ci_pipelines ALTER COLUMN auto_canceled_by_id TYPE integer;
+ ALTER TABLE ci_pipelines ALTER COLUMN auto_canceled_by_id_convert_to_bigint TYPE bigint;
+ SQL
+ end
+
+ it_behaves_like(
+ 'swap conversion columns',
+ table_name: :ci_pipelines,
+ from: :auto_canceled_by_id,
+ to: :auto_canceled_by_id_convert_to_bigint
+ )
+ end
+
+ context 'when auto_canceled_by_id sql type is bigint' do
+ before do
+ active_record_base.connection.execute(<<~SQL)
+ ALTER TABLE ci_pipelines ALTER COLUMN auto_canceled_by_id TYPE bigint;
+ ALTER TABLE ci_pipelines ALTER COLUMN auto_canceled_by_id_convert_to_bigint TYPE integer;
+ SQL
+ end
+
+ it 'does nothing' do
+ recorder = ActiveRecord::QueryRecorder.new { migrate! }
+ expect(recorder.log).not_to include(/LOCK TABLE/)
+ expect(recorder.log).not_to include(/ALTER TABLE/)
+ end
+ end
+end
diff --git a/spec/migrations/20231019084731_swap_columns_for_ci_stages_pipeline_id_bigint_v2_spec.rb b/spec/migrations/20231019084731_swap_columns_for_ci_stages_pipeline_id_bigint_v2_spec.rb
new file mode 100644
index 00000000000..266786dda3a
--- /dev/null
+++ b/spec/migrations/20231019084731_swap_columns_for_ci_stages_pipeline_id_bigint_v2_spec.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe SwapColumnsForCiStagesPipelineIdBigintV2, feature_category: :continuous_integration do
+ context 'when pipeline_id sql type is integer' do
+ before do
+ active_record_base.connection.execute(<<~SQL)
+ ALTER TABLE ci_stages ALTER COLUMN pipeline_id TYPE integer;
+ ALTER TABLE ci_stages ALTER COLUMN pipeline_id_convert_to_bigint TYPE bigint;
+ SQL
+ end
+
+ it_behaves_like(
+ 'swap conversion columns',
+ table_name: :ci_stages,
+ from: :pipeline_id,
+ to: :pipeline_id_convert_to_bigint
+ )
+ end
+
+ context 'when pipeline_id sql type is bigint' do
+ before do
+ active_record_base.connection.execute(<<~SQL)
+ ALTER TABLE ci_stages ALTER COLUMN pipeline_id TYPE bigint;
+ ALTER TABLE ci_stages ALTER COLUMN pipeline_id_convert_to_bigint TYPE integer;
+ SQL
+ end
+
+ it 'does nothing' do
+ recorder = ActiveRecord::QueryRecorder.new { migrate! }
+ expect(recorder.log).not_to include(/LOCK TABLE/)
+ expect(recorder.log).not_to include(/ALTER TABLE/)
+ end
+ end
+end
diff --git a/spec/migrations/20231019145202_add_status_to_packages_npm_metadata_caches_spec.rb b/spec/migrations/20231019145202_add_status_to_packages_npm_metadata_caches_spec.rb
new file mode 100644
index 00000000000..eeeafbdc277
--- /dev/null
+++ b/spec/migrations/20231019145202_add_status_to_packages_npm_metadata_caches_spec.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe AddStatusToPackagesNpmMetadataCaches, feature_category: :package_registry do
+ let(:npm_metadata_caches) { table(:packages_npm_metadata_caches) }
+
+ it 'correctly migrates up and down' do
+ reversible_migration do |migration|
+ migration.before -> {
+ expect(npm_metadata_caches.column_names).not_to include('status')
+ }
+
+ migration.after -> {
+ npm_metadata_caches.reset_column_information
+
+ expect(npm_metadata_caches.column_names).to include('status')
+ }
+ end
+ end
+end
diff --git a/spec/migrations/20231019223224_backfill_catalog_resources_name_and_description_spec.rb b/spec/migrations/20231019223224_backfill_catalog_resources_name_and_description_spec.rb
new file mode 100644
index 00000000000..2945b9fbf8e
--- /dev/null
+++ b/spec/migrations/20231019223224_backfill_catalog_resources_name_and_description_spec.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe BackfillCatalogResourcesNameAndDescription, feature_category: :pipeline_composition do
+ let(:namespace) { table(:namespaces).create!(name: 'name', path: 'path') }
+
+ let(:project) do
+ table(:projects).create!(
+ name: 'My project name', description: 'My description',
+ namespace_id: namespace.id, project_namespace_id: namespace.id
+ )
+ end
+
+ let(:resource) { table(:catalog_resources).create!(project_id: project.id) }
+
+ describe '#up' do
+ it 'updates the name and description to match the project' do
+ expect(resource.name).to be_nil
+ expect(resource.description).to be_nil
+
+ migrate!
+
+ expect(resource.reload.name).to eq(project.name)
+ expect(resource.reload.description).to eq(project.description)
+ end
+ end
+end
diff --git a/spec/migrations/20231020181652_add_index_packages_npm_metadata_caches_on_id_and_project_id_and_status_spec.rb b/spec/migrations/20231020181652_add_index_packages_npm_metadata_caches_on_id_and_project_id_and_status_spec.rb
new file mode 100644
index 00000000000..412ea33cb4e
--- /dev/null
+++ b/spec/migrations/20231020181652_add_index_packages_npm_metadata_caches_on_id_and_project_id_and_status_spec.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe AddIndexPackagesNpmMetadataCachesOnIdAndProjectIdAndStatus, feature_category: :package_registry do
+ let(:index_name) { described_class::INDEX_NAME }
+
+ it 'correctly migrates up and down' do
+ reversible_migration do |migration|
+ migration.before -> {
+ expect(ActiveRecord::Base.connection.indexes(:packages_npm_metadata_caches).map(&:name))
+ .not_to include(index_name)
+ }
+
+ migration.after -> {
+ # npm_metadata_caches.reset_column_information
+
+ expect(ActiveRecord::Base.connection.indexes(:packages_npm_metadata_caches).map(&:name))
+ .to include(index_name)
+ }
+ end
+ end
+end
diff --git a/spec/migrations/20231030071209_queue_backfill_packages_tags_project_id_spec.rb b/spec/migrations/20231030071209_queue_backfill_packages_tags_project_id_spec.rb
new file mode 100644
index 00000000000..98ce121c03b
--- /dev/null
+++ b/spec/migrations/20231030071209_queue_backfill_packages_tags_project_id_spec.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe QueueBackfillPackagesTagsProjectId, feature_category: :package_registry do
+ let!(:batched_migration) { described_class::MIGRATION }
+
+ it 'schedules a new batched migration' do
+ reversible_migration do |migration|
+ migration.before -> {
+ expect(batched_migration).not_to have_scheduled_batched_migration
+ }
+
+ migration.after -> {
+ expect(batched_migration).to have_scheduled_batched_migration(
+ table_name: :packages_tags,
+ column_name: :id,
+ interval: described_class::DELAY_INTERVAL,
+ batch_size: described_class::BATCH_SIZE,
+ sub_batch_size: described_class::SUB_BATCH_SIZE
+ )
+ }
+ end
+ end
+end
diff --git a/spec/migrations/20231102142554_migrate_zoekt_shards_to_zoekt_nodes_spec.rb b/spec/migrations/20231102142554_migrate_zoekt_shards_to_zoekt_nodes_spec.rb
new file mode 100644
index 00000000000..5f1d691f923
--- /dev/null
+++ b/spec/migrations/20231102142554_migrate_zoekt_shards_to_zoekt_nodes_spec.rb
@@ -0,0 +1,44 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe MigrateZoektShardsToZoektNodes, feature_category: :global_search do
+ let!(:migration) { described_class.new }
+
+ let(:attributes) do
+ {
+ index_base_url: "https://index.example.com",
+ search_base_url: "https://search.example.com",
+ uuid: SecureRandom.uuid,
+ used_bytes: 10,
+ total_bytes: 100
+ }.with_indifferent_access
+ end
+
+ let(:zoekt_shards) { table(:zoekt_shards) }
+ let(:zoekt_nodes) { table(:zoekt_nodes) }
+
+ let(:shard) do
+ zoekt_shards.create!(attributes)
+ end
+
+ let(:node) do
+ zoekt_nodes.create!(attributes)
+ end
+
+ describe '#up' do
+ it 'migrates zoekt_shard records to zoekt_nodes' do
+ shard
+ expect { migrate! }.to change { zoekt_nodes.count }.from(0).to(1)
+ expect(zoekt_nodes.first.attributes.with_indifferent_access).to include(attributes)
+ end
+ end
+
+ describe '#down' do
+ it 'deletes all zoekt_node records' do
+ node
+ expect { migration.down }.to change { zoekt_nodes.count }.from(1).to(0)
+ end
+ end
+end
diff --git a/spec/migrations/20231103223224_backfill_zoekt_node_id_on_indexed_namespaces_spec.rb b/spec/migrations/20231103223224_backfill_zoekt_node_id_on_indexed_namespaces_spec.rb
new file mode 100644
index 00000000000..60f08071af6
--- /dev/null
+++ b/spec/migrations/20231103223224_backfill_zoekt_node_id_on_indexed_namespaces_spec.rb
@@ -0,0 +1,79 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe BackfillZoektNodeIdOnIndexedNamespaces, feature_category: :global_search do
+ let!(:migration) { described_class.new }
+
+ let(:namespace) { table(:namespaces).create!(name: 'name', path: 'path') }
+
+ let(:zoekt_indexed_namespaces) { table(:zoekt_indexed_namespaces) }
+ let(:zoekt_shards) { table(:zoekt_shards) }
+ let(:zoekt_nodes) { table(:zoekt_nodes) }
+
+ let(:indexed_namespace) do
+ zoekt_indexed_namespaces.create!(
+ zoekt_shard_id: shard.id,
+ namespace_id: namespace.id
+ )
+ end
+
+ let(:attributes) do
+ {
+ index_base_url: "https://index.example.com",
+ search_base_url: "https://search.example.com",
+ uuid: SecureRandom.uuid,
+ used_bytes: 10,
+ total_bytes: 100
+ }.with_indifferent_access
+ end
+
+ let(:shard) do
+ zoekt_shards.create!(attributes)
+ end
+
+ let(:node) do
+ zoekt_nodes.create!(attributes)
+ end
+
+ describe '#up' do
+ it 'backfills zoekt_node_id with zoekt_shard_id' do
+ node
+ expect(indexed_namespace.zoekt_node_id).to be_nil
+ expect(indexed_namespace.zoekt_shard_id).to eq(shard.id)
+ migrate!
+ expect(indexed_namespace.reload.zoekt_node_id).to eq(node.id)
+ end
+
+ context 'when there is somehow more than one zoekt node' do
+ let(:node) do
+ zoekt_nodes.create!(
+ index_base_url: "https://index.example.com",
+ search_base_url: "https://search.example.com",
+ uuid: SecureRandom.uuid,
+ used_bytes: 10,
+ total_bytes: 100,
+ created_at: 5.days.ago
+ )
+ end
+
+ let(:node_2) do
+ zoekt_nodes.create!(
+ index_base_url: "https://index2.example.com",
+ search_base_url: "https://search2example.com",
+ uuid: SecureRandom.uuid,
+ used_bytes: 10,
+ total_bytes: 100
+ )
+ end
+
+ it 'uses the latest zoekt node' do
+ expect(node_2.created_at).to be > node.created_at
+ expect(indexed_namespace.zoekt_node_id).to be_nil
+ migrate!
+ expect(indexed_namespace.reload.zoekt_node_id).to eq(node_2.id)
+ end
+ end
+ end
+end
diff --git a/spec/migrations/db/migrate/20231103162825_add_wolfi_purl_type_to_package_metadata_purl_types_spec.rb b/spec/migrations/db/migrate/20231103162825_add_wolfi_purl_type_to_package_metadata_purl_types_spec.rb
new file mode 100644
index 00000000000..43ce53fffcb
--- /dev/null
+++ b/spec/migrations/db/migrate/20231103162825_add_wolfi_purl_type_to_package_metadata_purl_types_spec.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe AddWolfiPurlTypeToPackageMetadataPurlTypes, feature_category: :software_composition_analysis do
+ let(:settings) { table(:application_settings) }
+
+ describe "#up" do
+ it 'updates setting' do
+ settings.create!(package_metadata_purl_types: [1, 2, 4, 5, 9, 10])
+
+ disable_migrations_output do
+ migrate!
+ end
+
+ expect(ApplicationSetting.last.package_metadata_purl_types).to eq([1, 2, 4, 5, 9, 10, 13])
+ end
+ end
+
+ describe "#down" do
+ context 'with default value' do
+ it 'updates setting' do
+ settings.create!(package_metadata_purl_types: [1, 2, 4, 5, 9, 10, 13])
+
+ disable_migrations_output do
+ migrate!
+ schema_migrate_down!
+ end
+
+ expect(ApplicationSetting.last.package_metadata_purl_types).to eq([1, 2, 4, 5, 9, 10])
+ end
+ end
+ end
+end
diff --git a/spec/migrations/schedule_fixing_security_scan_statuses_spec.rb b/spec/migrations/schedule_fixing_security_scan_statuses_spec.rb
index 56d30e71676..f43f58d3be2 100644
--- a/spec/migrations/schedule_fixing_security_scan_statuses_spec.rb
+++ b/spec/migrations/schedule_fixing_security_scan_statuses_spec.rb
@@ -4,20 +4,21 @@ require 'spec_helper'
require_migration!
RSpec.describe ScheduleFixingSecurityScanStatuses,
- :suppress_gitlab_schemas_validate_connection, feature_category: :vulnerability_management do
+ :suppress_gitlab_schemas_validate_connection, :suppress_partitioning_routing_analyzer,
+ feature_category: :vulnerability_management do
let!(:namespaces) { table(:namespaces) }
let!(:projects) { table(:projects) }
- let!(:pipelines) { table(:ci_pipelines) }
- let!(:builds) { table(:ci_builds) }
+ let!(:pipelines) { table(:ci_pipelines, database: :ci) }
+ let!(:builds) { table(:ci_builds, database: :ci) { |model| model.primary_key = :id } }
let!(:security_scans) { table(:security_scans) }
let!(:namespace) { namespaces.create!(name: "foo", path: "bar") }
let!(:project) { projects.create!(namespace_id: namespace.id, project_namespace_id: namespace.id) }
let!(:pipeline) do
- pipelines.create!(project_id: project.id, ref: 'master', sha: 'adf43c3a', status: 'success', partition_id: 1)
+ pipelines.create!(project_id: project.id, ref: 'master', sha: 'adf43c3a', status: 'success', partition_id: 100)
end
- let!(:ci_build) { builds.create!(commit_id: pipeline.id, retried: false, type: 'Ci::Build', partition_id: 1) }
+ let!(:ci_build) { builds.create!(commit_id: pipeline.id, retried: false, type: 'Ci::Build', partition_id: 100) }
let!(:security_scan_1) { security_scans.create!(build_id: ci_build.id, scan_type: 1, created_at: 91.days.ago) }
let!(:security_scan_2) { security_scans.create!(build_id: ci_build.id, scan_type: 2) }
diff --git a/spec/models/activity_pub/releases_subscription_spec.rb b/spec/models/activity_pub/releases_subscription_spec.rb
new file mode 100644
index 00000000000..0c873a5c18a
--- /dev/null
+++ b/spec/models/activity_pub/releases_subscription_spec.rb
@@ -0,0 +1,79 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe ActivityPub::ReleasesSubscription, type: :model, feature_category: :release_orchestration do
+ describe 'factory' do
+ subject { build(:activity_pub_releases_subscription) }
+
+ it { is_expected.to be_valid }
+ end
+
+ describe 'associations' do
+ it { is_expected.to belong_to(:project).optional(false) }
+ end
+
+ describe 'validations' do
+ it { is_expected.to validate_presence_of(:subscriber_url) }
+
+ describe 'subscriber_url' do
+ subject { build(:activity_pub_releases_subscription) }
+
+ it { is_expected.to validate_uniqueness_of(:subscriber_url).case_insensitive.scoped_to([:project_id]) }
+ it { is_expected.to allow_value("http://example.com/actor").for(:subscriber_url) }
+ it { is_expected.not_to allow_values("I'm definitely not a URL").for(:subscriber_url) }
+ end
+
+ describe 'subscriber_inbox_url' do
+ subject { build(:activity_pub_releases_subscription) }
+
+ it { is_expected.to validate_uniqueness_of(:subscriber_inbox_url).case_insensitive.scoped_to([:project_id]) }
+ it { is_expected.to allow_value("http://example.com/actor").for(:subscriber_inbox_url) }
+ it { is_expected.not_to allow_values("I'm definitely not a URL").for(:subscriber_inbox_url) }
+ end
+
+ describe 'shared_inbox_url' do
+ subject { build(:activity_pub_releases_subscription) }
+
+ it { is_expected.to allow_value("http://example.com/actor").for(:shared_inbox_url) }
+ it { is_expected.not_to allow_values("I'm definitely not a URL").for(:shared_inbox_url) }
+ end
+
+ describe 'payload' do
+ it { is_expected.not_to allow_value("string").for(:payload) }
+ it { is_expected.not_to allow_value(1.0).for(:payload) }
+
+ it do
+ is_expected.to allow_value({
+ '@context': 'https://www.w3.org/ns/activitystreams',
+ id: 'https://example.com/actor#follow/1',
+ type: 'Follow',
+ actor: 'https://example.com/actor',
+ object: 'http://localhost/user/project/-/releases'
+ }).for(:payload)
+ end
+ end
+ end
+
+ describe '.find_by_subscriber_url' do
+ let_it_be(:subscription) { create(:activity_pub_releases_subscription) }
+
+ it 'returns a record if arguments match' do
+ result = described_class.find_by_subscriber_url(subscription.subscriber_url)
+
+ expect(result).to eq(subscription)
+ end
+
+ it 'returns a record if arguments match case insensitively' do
+ result = described_class.find_by_subscriber_url(subscription.subscriber_url.upcase)
+
+ expect(result).to eq(subscription)
+ end
+
+ it 'returns nil if project does not match' do
+ result = described_class.find_by_subscriber_url('I really should not exist')
+
+ expect(result).to be(nil)
+ end
+ end
+end
diff --git a/spec/models/ai/service_access_token_spec.rb b/spec/models/ai/service_access_token_spec.rb
index d979db4b3d6..d491735e604 100644
--- a/spec/models/ai/service_access_token_spec.rb
+++ b/spec/models/ai/service_access_token_spec.rb
@@ -2,10 +2,10 @@
require 'spec_helper'
-RSpec.describe Ai::ServiceAccessToken, type: :model, feature_category: :application_performance do
+RSpec.describe Ai::ServiceAccessToken, type: :model, feature_category: :cloud_connector do
describe '.expired', :freeze_time do
- let_it_be(:expired_token) { create(:service_access_token, :code_suggestions, :expired) }
- let_it_be(:active_token) { create(:service_access_token, :code_suggestions, :active) }
+ let_it_be(:expired_token) { create(:service_access_token, :expired) }
+ let_it_be(:active_token) { create(:service_access_token, :active) }
it 'selects all expired tokens' do
expect(described_class.expired).to match_array([expired_token])
@@ -13,24 +13,14 @@ RSpec.describe Ai::ServiceAccessToken, type: :model, feature_category: :applicat
end
describe '.active', :freeze_time do
- let_it_be(:expired_token) { create(:service_access_token, :code_suggestions, :expired) }
- let_it_be(:active_token) { create(:service_access_token, :code_suggestions, :active) }
+ let_it_be(:expired_token) { create(:service_access_token, :expired) }
+ let_it_be(:active_token) { create(:service_access_token, :active) }
it 'selects all active tokens' do
expect(described_class.active).to match_array([active_token])
end
end
- # There is currently only one category, please expand this test when a new category is added.
- describe '.for_category' do
- let(:code_suggestions_token) { create(:service_access_token, :code_suggestions) }
- let(:category) { :code_suggestions }
-
- it 'only selects tokens from the selected category' do
- expect(described_class.for_category(category)).to match_array([code_suggestions_token])
- end
- end
-
describe '#token' do
let(:token_value) { 'Abc' }
@@ -47,7 +37,6 @@ RSpec.describe Ai::ServiceAccessToken, type: :model, feature_category: :applicat
describe 'validations' do
it { is_expected.to validate_presence_of(:token) }
- it { is_expected.to validate_presence_of(:category) }
it { is_expected.to validate_presence_of(:expires_at) }
end
end
diff --git a/spec/models/alert_management/http_integration_spec.rb b/spec/models/alert_management/http_integration_spec.rb
index dc26d0323d7..ef9eaa960f2 100644
--- a/spec/models/alert_management/http_integration_spec.rb
+++ b/spec/models/alert_management/http_integration_spec.rb
@@ -44,8 +44,8 @@ RSpec.describe AlertManagement::HttpIntegration, feature_category: :incident_man
context 'with valid JSON schema' do
let(:attribute_mapping) do
{
- title: { path: %w(a b c), type: 'string', label: 'Title' },
- description: { path: %w(a), type: 'string' }
+ title: { path: %w[a b c], type: 'string', label: 'Title' },
+ description: { path: %w[a], type: 'string' }
}
end
@@ -78,7 +78,7 @@ RSpec.describe AlertManagement::HttpIntegration, feature_category: :incident_man
context 'when property has extra attributes' do
let(:attribute_mapping) do
- { title: { path: %w(a b c), type: 'string', extra: 'property' } }
+ { title: { path: %w[a b c], type: 'string', extra: 'property' } }
end
it_behaves_like 'is invalid record'
diff --git a/spec/models/analytics/cycle_analytics/value_stream_spec.rb b/spec/models/analytics/cycle_analytics/value_stream_spec.rb
index 3b3187e0b51..852ace6a920 100644
--- a/spec/models/analytics/cycle_analytics/value_stream_spec.rb
+++ b/spec/models/analytics/cycle_analytics/value_stream_spec.rb
@@ -99,4 +99,23 @@ RSpec.describe Analytics::CycleAnalytics::ValueStream, type: :model, feature_cat
it { is_expected.to be_custom }
end
end
+
+ describe '#project' do
+ subject(:value_stream) do
+ build(:cycle_analytics_value_stream, name: 'value_stream_1', namespace: namespace).project
+ end
+
+ context 'when namespace is a project' do
+ let_it_be(:project) { create(:project) }
+ let(:namespace) { project.project_namespace }
+
+ it { is_expected.to eq(project) }
+ end
+
+ context 'when namespace is a group' do
+ let_it_be(:namespace) { create(:group) }
+
+ it { is_expected.to be_nil }
+ end
+ end
end
diff --git a/spec/models/appearance_spec.rb b/spec/models/appearance_spec.rb
index b5f47c950b9..ffb46884e5d 100644
--- a/spec/models/appearance_spec.rb
+++ b/spec/models/appearance_spec.rb
@@ -81,7 +81,7 @@ RSpec.describe Appearance do
end
end
- %i(logo header_logo pwa_icon favicon).each do |logo_type|
+ %i[logo header_logo pwa_icon favicon].each do |logo_type|
it_behaves_like 'logo paths', logo_type
end
diff --git a/spec/models/application_setting_spec.rb b/spec/models/application_setting_spec.rb
index 78bf410075b..a2d6c60fbd0 100644
--- a/spec/models/application_setting_spec.rb
+++ b/spec/models/application_setting_spec.rb
@@ -162,6 +162,8 @@ RSpec.describe ApplicationSetting, feature_category: :shared, type: :model do
it { is_expected.to validate_inclusion_of(:user_defaults_to_private_profile).in_array([true, false]) }
+ it { is_expected.to validate_inclusion_of(:allow_project_creation_for_guest_and_below).in_array([true, false]) }
+
it { is_expected.to validate_inclusion_of(:deny_all_requests_except_allowed).in_array([true, false]) }
it 'ensures max_pages_size is an integer greater than 0 (or equal to 0 to indicate unlimited/maximum)' do
@@ -254,11 +256,11 @@ RSpec.describe ApplicationSetting, feature_category: :shared, type: :model do
it { is_expected.not_to allow_value(['']).for(:valid_runner_registrars) }
it { is_expected.not_to allow_value(['OBVIOUSLY_WRONG']).for(:valid_runner_registrars) }
- it { is_expected.not_to allow_value(%w(project project)).for(:valid_runner_registrars) }
+ it { is_expected.not_to allow_value(%w[project project]).for(:valid_runner_registrars) }
it { is_expected.not_to allow_value([nil]).for(:valid_runner_registrars) }
it { is_expected.not_to allow_value(nil).for(:valid_runner_registrars) }
it { is_expected.to allow_value([]).for(:valid_runner_registrars) }
- it { is_expected.to allow_value(%w(project group)).for(:valid_runner_registrars) }
+ it { is_expected.to allow_value(%w[project group]).for(:valid_runner_registrars) }
it { is_expected.to allow_value(http).for(:jira_connect_proxy_url) }
it { is_expected.to allow_value(https).for(:jira_connect_proxy_url) }
@@ -820,15 +822,6 @@ RSpec.describe ApplicationSetting, feature_category: :shared, type: :model do
subject { setting }
end
- # Upgraded databases will have this sort of content
- context 'repository_storages is a String, not an Array' do
- before do
- described_class.where(id: setting.id).update_all(repository_storages: 'default')
- end
-
- it { expect(setting.repository_storages).to eq(['default']) }
- end
-
context 'auto_devops_domain setting' do
context 'when auto_devops_enabled? is true' do
before do
@@ -865,31 +858,6 @@ RSpec.describe ApplicationSetting, feature_category: :shared, type: :model do
end
end
- context 'repository storages' do
- before do
- storages = {
- 'custom1' => 'tmp/tests/custom_repositories_1',
- 'custom2' => 'tmp/tests/custom_repositories_2',
- 'custom3' => 'tmp/tests/custom_repositories_3'
-
- }
- allow(Gitlab.config.repositories).to receive(:storages).and_return(storages)
- end
-
- describe 'inclusion' do
- it { is_expected.to allow_value('custom1').for(:repository_storages) }
- it { is_expected.to allow_value(%w(custom2 custom3)).for(:repository_storages) }
- it { is_expected.not_to allow_value('alternative').for(:repository_storages) }
- it { is_expected.not_to allow_value(%w(alternative custom1)).for(:repository_storages) }
- end
-
- describe 'presence' do
- it { is_expected.not_to allow_value([]).for(:repository_storages) }
- it { is_expected.not_to allow_value("").for(:repository_storages) }
- it { is_expected.not_to allow_value(nil).for(:repository_storages) }
- end
- end
-
context 'housekeeping settings' do
it { is_expected.not_to allow_value(0).for(:housekeeping_optimize_repository_period) }
end
diff --git a/spec/models/authentication_event_spec.rb b/spec/models/authentication_event_spec.rb
index 17fe10b5b4e..8ce949c737b 100644
--- a/spec/models/authentication_event_spec.rb
+++ b/spec/models/authentication_event_spec.rb
@@ -37,11 +37,11 @@ RSpec.describe AuthenticationEvent do
describe '.providers' do
before do
- allow(Devise).to receive(:omniauth_providers).and_return(%w(ldapmain google_oauth2))
+ allow(Devise).to receive(:omniauth_providers).and_return(%w[ldapmain google_oauth2])
end
it 'returns an array of distinct providers' do
- expect(described_class.providers).to match_array %w(ldapmain google_oauth2 standard two-factor two-factor-via-u2f-device two-factor-via-webauthn-device)
+ expect(described_class.providers).to match_array %w[ldapmain google_oauth2 standard two-factor two-factor-via-u2f-device two-factor-via-webauthn-device]
end
end
diff --git a/spec/models/blob_viewer/base_spec.rb b/spec/models/blob_viewer/base_spec.rb
index 682b6dc3b1d..91c32d5c7c9 100644
--- a/spec/models/blob_viewer/base_spec.rb
+++ b/spec/models/blob_viewer/base_spec.rb
@@ -11,7 +11,7 @@ RSpec.describe BlobViewer::Base do
Class.new(described_class) do
include BlobViewer::ServerSide
- self.extensions = %w(pdf)
+ self.extensions = %w[pdf]
self.binary = true
self.collapse_limit = 1.megabyte
self.size_limit = 5.megabytes
@@ -41,7 +41,7 @@ RSpec.describe BlobViewer::Base do
context 'when the file type is supported' do
before do
- viewer_class.file_types = %i(license)
+ viewer_class.file_types = %i[license]
viewer_class.binary = false
end
diff --git a/spec/models/bulk_imports/entity_spec.rb b/spec/models/bulk_imports/entity_spec.rb
index 3e98ba0973e..b822786579b 100644
--- a/spec/models/bulk_imports/entity_spec.rb
+++ b/spec/models/bulk_imports/entity_spec.rb
@@ -248,6 +248,24 @@ RSpec.describe BulkImports::Entity, type: :model, feature_category: :importers d
end
end
+ describe '#portable_class' do
+ context 'when entity is group' do
+ it 'returns Group class' do
+ entity = build(:bulk_import_entity, :group_entity)
+
+ expect(entity.portable_class).to eq(Group)
+ end
+ end
+
+ context 'when entity is project' do
+ it 'returns Project class' do
+ entity = build(:bulk_import_entity, :project_entity)
+
+ expect(entity.portable_class).to eq(Project)
+ end
+ end
+ end
+
describe '#export_relations_url_path' do
context 'when entity is group' do
it 'returns group export relations url' do
diff --git a/spec/models/bulk_imports/failure_spec.rb b/spec/models/bulk_imports/failure_spec.rb
index b3fd60ba348..928f14aaced 100644
--- a/spec/models/bulk_imports/failure_spec.rb
+++ b/spec/models/bulk_imports/failure_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe BulkImports::Failure, type: :model do
+RSpec.describe BulkImports::Failure, type: :model, feature_category: :importers do
let(:failure) { create(:bulk_import_failure) }
describe 'associations' do
@@ -44,4 +44,18 @@ RSpec.describe BulkImports::Failure, type: :model do
end
end
end
+
+ describe '#exception_message=' do
+ it 'filters file paths' do
+ failure = described_class.new
+ failure.exception_message = 'Failed to read /FILE/PATH'
+ expect(failure.exception_message).to eq('Failed to read [FILTERED]')
+ end
+
+ it 'truncates long string' do
+ failure = described_class.new
+ failure.exception_message = 'A' * 1000
+ expect(failure.exception_message.size).to eq(255)
+ end
+ end
end
diff --git a/spec/models/ci/build_dependencies_spec.rb b/spec/models/ci/build_dependencies_spec.rb
index ab32234eba3..f80af7b9dbc 100644
--- a/spec/models/ci/build_dependencies_spec.rb
+++ b/spec/models/ci/build_dependencies_spec.rb
@@ -107,7 +107,7 @@ RSpec.describe Ci::BuildDependencies, feature_category: :continuous_integration
end
context 'when dependencies are defined' do
- let(:dependencies) { %w(rspec staging) }
+ let(:dependencies) { %w[rspec staging] }
it { is_expected.to contain_exactly(rspec_test, staging) }
end
@@ -137,7 +137,7 @@ RSpec.describe Ci::BuildDependencies, feature_category: :continuous_integration
end
context 'when needs and dependencies are defined' do
- let(:dependencies) { %w(rspec staging) }
+ let(:dependencies) { %w[rspec staging] }
let(:needs) do
[
{ name: 'build', artifacts: true },
@@ -150,7 +150,7 @@ RSpec.describe Ci::BuildDependencies, feature_category: :continuous_integration
end
context 'when needs and dependencies contradict' do
- let(:dependencies) { %w(rspec staging) }
+ let(:dependencies) { %w[rspec staging] }
let(:needs) do
[
{ name: 'build', artifacts: true },
diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb
index 2a5d781edc7..2e552c8d524 100644
--- a/spec/models/ci/build_spec.rb
+++ b/spec/models/ci/build_spec.rb
@@ -87,7 +87,7 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration, factory_def
describe 'status' do
context 'when transitioning to any state from running' do
it 'removes runner_session' do
- %w(success drop cancel).each do |event|
+ %w[success drop cancel].each do |event|
build = FactoryBot.create(:ci_build, :running, :with_runner_session, pipeline: pipeline)
build.fire_events!(event)
@@ -1090,7 +1090,7 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration, factory_def
let(:options_with_fallback_keys) do
{ cache: [
- { key: "key", paths: ["public"], policy: "pull-push", fallback_keys: %w(key1 key2) }
+ { key: "key", paths: ["public"], policy: "pull-push", fallback_keys: %w[key1 key2] }
] }
end
@@ -1111,8 +1111,8 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration, factory_def
let(:options_with_fallback_keys) do
{ cache: [
- { key: "key", paths: ["public"], policy: "pull-push", fallback_keys: %w(key3 key4) },
- { key: "key2", paths: ["public"], policy: "pull-push", fallback_keys: %w(key5 key6) }
+ { key: "key", paths: ["public"], policy: "pull-push", fallback_keys: %w[key3 key4] },
+ { key: "key2", paths: ["public"], policy: "pull-push", fallback_keys: %w[key5 key6] }
] }
end
@@ -1214,11 +1214,11 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration, factory_def
is_expected.to match([
a_hash_including({
key: 'key-1',
- fallback_keys: %w(key3-1 key4-1)
+ fallback_keys: %w[key3-1 key4-1]
}),
a_hash_including({
key: 'key2-1',
- fallback_keys: %w(key5-1 key6-1)
+ fallback_keys: %w[key5-1 key6-1]
})
])
end
@@ -1241,11 +1241,11 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration, factory_def
is_expected.to match([
a_hash_including({
key: 'key-1',
- fallback_keys: %w(key3-1 key4-1)
+ fallback_keys: %w[key3-1 key4-1]
}),
a_hash_including({
key: 'key2-1',
- fallback_keys: %w(key5-1 key6-1)
+ fallback_keys: %w[key5-1 key6-1]
})
])
end
@@ -1320,7 +1320,7 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration, factory_def
allow(build).to receive(:options).and_return({
cache: [{
key: "key1",
- fallback_keys: %w(key2)
+ fallback_keys: %w[key2]
}]
})
end
@@ -2230,7 +2230,7 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration, factory_def
end
context 'when artifacts do not expire' do
- it { is_expected.to eq(false) }
+ it { is_expected.to be_falsey }
end
context 'when artifacts expire in the future' do
@@ -2951,7 +2951,7 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration, factory_def
end
context 'when runner is assigned to build' do
- let(:runner) { create(:ci_runner, description: 'description', tag_list: %w(docker linux)) }
+ let(:runner) { create(:ci_runner, description: 'description', tag_list: %w[docker linux]) }
before do
build.update!(runner: runner)
@@ -3952,8 +3952,8 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration, factory_def
end
context 'when have different tags' do
- let(:build_tag_list) { %w(A B) }
- let(:tag_list) { %w(C D) }
+ let(:build_tag_list) { %w[A B] }
+ let(:tag_list) { %w[C D] }
it "does not match a build" do
is_expected.not_to contain_exactly(build)
@@ -3961,8 +3961,8 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration, factory_def
end
context 'when have a subset of tags' do
- let(:build_tag_list) { %w(A B) }
- let(:tag_list) { %w(A B C D) }
+ let(:build_tag_list) { %w[A B] }
+ let(:tag_list) { %w[A B C D] }
it "does match a build" do
is_expected.to contain_exactly(build)
@@ -3971,7 +3971,7 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration, factory_def
context 'when build does not have tags' do
let(:build_tag_list) { [] }
- let(:tag_list) { %w(C D) }
+ let(:tag_list) { %w[C D] }
it "does match a build" do
is_expected.to contain_exactly(build)
@@ -3979,8 +3979,8 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration, factory_def
end
context 'when does not have a subset of tags' do
- let(:build_tag_list) { %w(A B C) }
- let(:tag_list) { %w(C D) }
+ let(:build_tag_list) { %w[A B C] }
+ let(:tag_list) { %w[C D] }
it "does not match a build" do
is_expected.not_to contain_exactly(build)
@@ -3998,7 +3998,7 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration, factory_def
end
context 'when does have tags' do
- let(:tag_list) { %w(A B) }
+ let(:tag_list) { %w[A B] }
it "does match a build" do
is_expected.to contain_exactly(build)
@@ -4676,7 +4676,7 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration, factory_def
describe '#invalid_dependencies' do
let!(:pre_stage_job_valid) { create(:ci_build, :manual, pipeline: pipeline, name: 'test1', stage_idx: 0) }
let!(:pre_stage_job_invalid) { create(:ci_build, :success, :expired, pipeline: pipeline, name: 'test2', stage_idx: 1) }
- let!(:job) { create(:ci_build, :pending, pipeline: pipeline, stage_idx: 2, options: { dependencies: %w(test1 test2) }) }
+ let!(:job) { create(:ci_build, :pending, pipeline: pipeline, stage_idx: 2, options: { dependencies: %w[test1 test2] }) }
context 'when pipeline is locked' do
before do
@@ -5229,16 +5229,34 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration, factory_def
subject { build.doom! }
let(:traits) { [] }
- let(:build) { create(:ci_build, *traits, pipeline: pipeline) }
+ let(:build) do
+ travel(-1.minute) do
+ create(:ci_build, *traits, pipeline: pipeline)
+ end
+ end
- it 'updates status and failure_reason', :aggregate_failures do
- subject
+ it 'updates status, failure_reason, finished_at and updated_at', :aggregate_failures do
+ old_timestamp = build.updated_at
+ new_timestamp = \
+ freeze_time do
+ Time.current.tap do
+ subject
+ end
+ end
+
+ expect(old_timestamp).not_to eq(new_timestamp)
+ expect(build.updated_at).to eq(new_timestamp)
+ expect(build.finished_at).to eq(new_timestamp)
expect(build.status).to eq("failed")
expect(build.failure_reason).to eq("data_integrity_failure")
end
- it 'logs a message' do
+ it 'logs a message and increments the job failure counter', :aggregate_failures do
+ expect(::Gitlab::Ci::Pipeline::Metrics.job_failure_reason_counter)
+ .to(receive(:increment))
+ .with(reason: :data_integrity_failure)
+
expect(Gitlab::AppLogger)
.to receive(:info)
.with(a_hash_including(message: 'Build doomed', class: build.class.name, build_id: build.id))
@@ -5273,12 +5291,20 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration, factory_def
context 'with running builds' do
let(:traits) { [:picked] }
- it 'drops associated runtime metadata' do
+ it 'drops associated runtime metadata', :aggregate_failures do
subject
expect(build.reload.runtime_metadata).not_to be_present
end
end
+
+ context 'finished builds' do
+ let(:traits) { [:finished] }
+
+ it 'does not update finished_at' do
+ expect { subject }.not_to change { build.reload.finished_at }
+ end
+ end
end
it 'does not generate cross DB queries when a record is created via FactoryBot' do
diff --git a/spec/models/ci/build_trace_chunks/redis_spec.rb b/spec/models/ci/build_trace_chunks/redis_spec.rb
index 0d8cda7b3d8..25a24f4946b 100644
--- a/spec/models/ci/build_trace_chunks/redis_spec.rb
+++ b/spec/models/ci/build_trace_chunks/redis_spec.rb
@@ -4,223 +4,8 @@ require 'spec_helper'
RSpec.describe Ci::BuildTraceChunks::Redis, :clean_gitlab_redis_shared_state do
let(:data_store) { described_class.new }
+ let(:store_trait_with_data) { :redis_with_data }
+ let(:store_trait_without_data) { :redis_without_data }
- describe '#data' do
- subject { data_store.data(model) }
-
- context 'when data exists' do
- let(:model) { create(:ci_build_trace_chunk, :redis_with_data, initial_data: 'sample data in redis') }
-
- it 'returns the data' do
- is_expected.to eq('sample data in redis')
- end
- end
-
- context 'when data does not exist' do
- let(:model) { create(:ci_build_trace_chunk, :redis_without_data) }
-
- it 'returns nil' do
- is_expected.to be_nil
- end
- end
- end
-
- describe '#set_data' do
- subject { data_store.set_data(model, data) }
-
- let(:data) { 'abc123' }
-
- context 'when data exists' do
- let(:model) { create(:ci_build_trace_chunk, :redis_with_data, initial_data: 'sample data in redis') }
-
- it 'overwrites data' do
- expect(data_store.data(model)).to eq('sample data in redis')
-
- subject
-
- expect(data_store.data(model)).to eq('abc123')
- end
- end
-
- context 'when data does not exist' do
- let(:model) { create(:ci_build_trace_chunk, :redis_without_data) }
-
- it 'sets new data' do
- expect(data_store.data(model)).to be_nil
-
- subject
-
- expect(data_store.data(model)).to eq('abc123')
- end
- end
- end
-
- describe '#append_data' do
- context 'when valid offset is used with existing data' do
- let(:model) { create(:ci_build_trace_chunk, :redis_with_data, initial_data: 'abcd') }
-
- it 'appends data' do
- expect(data_store.data(model)).to eq('abcd')
-
- length = data_store.append_data(model, '12345', 4)
-
- expect(length).to eq 9
- expect(data_store.data(model)).to eq('abcd12345')
- end
- end
-
- context 'when data does not exist yet' do
- let(:model) { create(:ci_build_trace_chunk, :redis_without_data) }
-
- it 'sets new data' do
- expect(data_store.data(model)).to be_nil
-
- length = data_store.append_data(model, 'abc', 0)
-
- expect(length).to eq 3
- expect(data_store.data(model)).to eq('abc')
- end
- end
-
- context 'when data needs to be truncated' do
- let(:model) { create(:ci_build_trace_chunk, :redis_with_data, initial_data: '12345678') }
-
- it 'appends data and truncates stored value' do
- expect(data_store.data(model)).to eq('12345678')
-
- length = data_store.append_data(model, 'ab', 4)
-
- expect(length).to eq 6
- expect(data_store.data(model)).to eq('1234ab')
- end
- end
-
- context 'when invalid offset is provided' do
- let(:model) { create(:ci_build_trace_chunk, :redis_with_data, initial_data: 'abc') }
-
- it 'raises an exception' do
- length = data_store.append_data(model, '12345', 4)
-
- expect(length).to be_negative
- end
- end
-
- context 'when trace contains multi-byte UTF8 characters' do
- let(:model) { create(:ci_build_trace_chunk, :redis_with_data, initial_data: 'aüc') }
-
- it 'appends data' do
- length = data_store.append_data(model, '1234', 4)
-
- data_store.data(model).then do |new_data|
- expect(new_data.bytesize).to eq 8
- expect(new_data).to eq 'aüc1234'
- end
-
- expect(length).to eq 8
- end
- end
-
- context 'when trace contains non-UTF8 characters' do
- let(:model) { create(:ci_build_trace_chunk, :redis_with_data, initial_data: "a\255c") }
-
- it 'appends data' do
- length = data_store.append_data(model, '1234', 3)
-
- data_store.data(model).then do |new_data|
- expect(new_data.bytesize).to eq 7
- end
-
- expect(length).to eq 7
- end
- end
- end
-
- describe '#delete_data' do
- subject { data_store.delete_data(model) }
-
- context 'when data exists' do
- let(:model) { create(:ci_build_trace_chunk, :redis_with_data, initial_data: 'sample data in redis') }
-
- it 'deletes data' do
- expect(data_store.data(model)).to eq('sample data in redis')
-
- subject
-
- expect(data_store.data(model)).to be_nil
- end
- end
-
- context 'when data does not exist' do
- let(:model) { create(:ci_build_trace_chunk, :redis_without_data) }
-
- it 'does nothing' do
- expect(data_store.data(model)).to be_nil
-
- subject
-
- expect(data_store.data(model)).to be_nil
- end
- end
- end
-
- describe '#size' do
- context 'when data exists' do
- let(:model) { create(:ci_build_trace_chunk, :redis_with_data, initial_data: 'üabcd') }
-
- it 'returns data bytesize correctly' do
- expect(data_store.size(model)).to eq 6
- end
- end
-
- context 'when data does not exist' do
- let(:model) { create(:ci_build_trace_chunk, :redis_without_data) }
-
- it 'returns zero' do
- expect(data_store.size(model)).to be_zero
- end
- end
- end
-
- describe '#keys' do
- subject { data_store.keys(relation) }
-
- let(:build) { create(:ci_build) }
- let(:relation) { build.trace_chunks }
-
- before do
- create(:ci_build_trace_chunk, :redis_with_data, chunk_index: 0, build: build)
- create(:ci_build_trace_chunk, :redis_with_data, chunk_index: 1, build: build)
- end
-
- it 'returns keys' do
- is_expected.to eq([[build.id, 0], [build.id, 1]])
- end
- end
-
- describe '#delete_keys' do
- subject { data_store.delete_keys(keys) }
-
- let(:build) { create(:ci_build) }
- let(:relation) { build.trace_chunks }
- let(:keys) { data_store.keys(relation) }
-
- before do
- create(:ci_build_trace_chunk, :redis_with_data, chunk_index: 0, build: build)
- create(:ci_build_trace_chunk, :redis_with_data, chunk_index: 1, build: build)
- end
-
- it 'deletes multiple data' do
- Gitlab::Redis::SharedState.with do |redis|
- expect(redis.exists?("gitlab:ci:trace:#{build.id}:chunks:0")).to eq(true)
- expect(redis.exists?("gitlab:ci:trace:#{build.id}:chunks:1")).to eq(true)
- end
-
- subject
-
- Gitlab::Redis::SharedState.with do |redis|
- expect(redis.exists?("gitlab:ci:trace:#{build.id}:chunks:0")).to eq(false)
- expect(redis.exists?("gitlab:ci:trace:#{build.id}:chunks:1")).to eq(false)
- end
- end
- end
+ it_behaves_like 'CI build trace chunk redis', Gitlab::Redis::SharedState
end
diff --git a/spec/models/ci/build_trace_chunks/redis_trace_chunks_spec.rb b/spec/models/ci/build_trace_chunks/redis_trace_chunks_spec.rb
new file mode 100644
index 00000000000..eb2226e39ca
--- /dev/null
+++ b/spec/models/ci/build_trace_chunks/redis_trace_chunks_spec.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Ci::BuildTraceChunks::RedisTraceChunks, :clean_gitlab_redis_trace_chunks,
+ feature_category: :continuous_integration do
+ let(:data_store) { described_class.new }
+ let(:store_trait_with_data) { :redis_trace_chunks_with_data }
+ let(:store_trait_without_data) { :redis_trace_chunks_without_data }
+
+ it_behaves_like 'CI build trace chunk redis', Gitlab::Redis::TraceChunks
+end
diff --git a/spec/models/ci/catalog/components_project_spec.rb b/spec/models/ci/catalog/components_project_spec.rb
index d7e0ee2079c..79e1a113e47 100644
--- a/spec/models/ci/catalog/components_project_spec.rb
+++ b/spec/models/ci/catalog/components_project_spec.rb
@@ -5,32 +5,29 @@ require 'spec_helper'
RSpec.describe Ci::Catalog::ComponentsProject, feature_category: :pipeline_composition do
using RSpec::Parameterized::TableSyntax
- let_it_be(:files) do
- {
- 'templates/secret-detection.yml' => "spec:\n inputs:\n website:\n---\nimage: alpine_1",
- 'templates/dast/template.yml' => 'image: alpine_2',
- 'templates/template.yml' => 'image: alpine_3',
- 'templates/blank-yaml.yml' => '',
- 'templates/dast/sub-folder/template.yml' => 'image: alpine_4',
- 'tests/test.yml' => 'image: alpine_5',
- 'README.md' => 'Read me'
- }
- end
-
- let_it_be(:project) do
- create(
- :project, :custom_repo,
- description: 'Simple, complex, and other components',
- files: files
- )
- end
-
+ let_it_be(:project) { create(:project, :catalog_resource_with_components) }
let_it_be(:catalog_resource) { create(:ci_catalog_resource, project: project) }
let(:components_project) { described_class.new(project, project.default_branch) }
describe '#fetch_component_paths' do
- it 'retrieves all the paths for valid components' do
+ context 'when there are invalid paths' do
+ let(:project) do
+ create(:project, :small_repo, description: 'description',
+ files: { 'templates/secrets.yml' => '',
+ 'tests/test.yml' => 'this is invalid',
+ 'README.md' => 'this is not ok' }
+ )
+ end
+
+ it 'does not retrieve the invalid path(s) and only retrieves the valid one(s)' do
+ paths = components_project.fetch_component_paths(project.default_branch)
+
+ expect(paths).to contain_exactly('templates/secrets.yml')
+ end
+ end
+
+ it 'retrieves all the valid paths for components' do
paths = components_project.fetch_component_paths(project.default_branch)
expect(paths).to contain_exactly(
@@ -38,6 +35,12 @@ RSpec.describe Ci::Catalog::ComponentsProject, feature_category: :pipeline_compo
'templates/template.yml'
)
end
+
+ it 'does not fetch more paths than the limit' do
+ paths = components_project.fetch_component_paths(project.default_branch, limit: 1)
+
+ expect(paths.size).to eq(1)
+ end
end
describe '#extract_component_name' do
@@ -50,7 +53,11 @@ RSpec.describe Ci::Catalog::ComponentsProject, feature_category: :pipeline_compo
context 'with valid component paths' do
where(:path, :name) do
'templates/secret-detection.yml' | 'secret-detection'
+ 'templates/secret_detection.yml' | 'secret_detection'
+ 'templates/secret_detection123.yml' | 'secret_detection123'
+ 'templates/secret-detection-123.yml' | 'secret-detection-123'
'templates/dast/template.yml' | 'dast'
+ 'templates/d-a-s_t/template.yml' | 'd-a-s_t'
'templates/template.yml' | 'template'
'templates/blank-yaml.yml' | 'blank-yaml'
end
diff --git a/spec/models/ci/catalog/listing_spec.rb b/spec/models/ci/catalog/listing_spec.rb
index 7524d908252..7a1e12165ac 100644
--- a/spec/models/ci/catalog/listing_spec.rb
+++ b/spec/models/ci/catalog/listing_spec.rb
@@ -4,109 +4,179 @@ require 'spec_helper'
RSpec.describe Ci::Catalog::Listing, feature_category: :pipeline_composition do
let_it_be(:namespace) { create(:group) }
- let_it_be(:project_1) { create(:project, namespace: namespace, name: 'X Project') }
- let_it_be(:project_2) { create(:project, namespace: namespace, name: 'B Project') }
- let_it_be(:project_3) { create(:project, namespace: namespace, name: 'A Project') }
- let_it_be(:project_4) { create(:project) }
+ let_it_be(:project_x) { create(:project, namespace: namespace, name: 'X Project') }
+ let_it_be(:project_a) { create(:project, :public, namespace: namespace, name: 'A Project') }
+ let_it_be(:project_noaccess) { create(:project, namespace: namespace, name: 'C Project') }
+ let_it_be(:project_ext) { create(:project, :public, name: 'TestProject') }
let_it_be(:user) { create(:user) }
- let(:list) { described_class.new(namespace, user) }
+ let_it_be(:project_b) do
+ create(:project, namespace: namespace, name: 'B Project', description: 'Rspec test framework')
+ end
- describe '#new' do
- context 'when namespace is not a root namespace' do
- let(:namespace) { create(:group, :nested) }
+ let(:list) { described_class.new(user) }
- it 'raises an exception' do
- expect { list }.to raise_error(ArgumentError, 'Namespace is not a root namespace')
- end
- end
+ before_all do
+ project_x.add_reporter(user)
+ project_b.add_reporter(user)
+ project_a.add_reporter(user)
+ project_ext.add_reporter(user)
end
describe '#resources' do
- subject(:resources) { list.resources }
+ subject(:resources) { list.resources(**params) }
+
+ context 'when user is anonymous' do
+ let(:user) { nil }
+ let(:params) { {} }
+
+ let!(:resource_1) { create(:ci_catalog_resource, project: project_a) }
+ let!(:resource_2) { create(:ci_catalog_resource, project: project_ext) }
+ let!(:resource_3) { create(:ci_catalog_resource, project: project_b) }
+
+ it 'returns only resources for public projects' do
+ is_expected.to contain_exactly(resource_1, resource_2)
+ end
+
+ context 'when sorting is provided' do
+ let(:params) { { sort: :name_desc } }
+
+ it 'returns only resources for public projects sorted by name DESC' do
+ is_expected.to contain_exactly(resource_2, resource_1)
+ end
+ end
+ end
+
+ context 'when search params are provided' do
+ let(:params) { { search: 'test' } }
+
+ let!(:resource_1) { create(:ci_catalog_resource, project: project_a) }
+ let!(:resource_2) { create(:ci_catalog_resource, project: project_ext) }
+ let!(:resource_3) { create(:ci_catalog_resource, project: project_b) }
- context 'when the user has access to all projects in the namespace' do
- before do
- namespace.add_developer(user)
+ it 'returns the resources that match the search params' do
+ is_expected.to contain_exactly(resource_2, resource_3)
end
- context 'when the namespace has no catalog resources' do
+ context 'when search term is too small' do
+ let(:params) { { search: 'te' } }
+
it { is_expected.to be_empty }
end
+ end
- context 'when the namespace has catalog resources' do
- let_it_be(:today) { Time.zone.now }
- let_it_be(:yesterday) { today - 1.day }
- let_it_be(:tomorrow) { today + 1.day }
+ context 'when namespace is provided' do
+ let(:params) { { namespace: namespace } }
- let_it_be(:resource) { create(:ci_catalog_resource, project: project_1, latest_released_at: yesterday) }
- let_it_be(:resource_2) { create(:ci_catalog_resource, project: project_2, latest_released_at: today) }
- let_it_be(:resource_3) { create(:ci_catalog_resource, project: project_3, latest_released_at: nil) }
+ context 'when namespace is not a root namespace' do
+ let(:namespace) { create(:group, :nested) }
- let_it_be(:other_namespace_resource) do
- create(:ci_catalog_resource, project: project_4, latest_released_at: tomorrow)
+ it 'raises an exception' do
+ expect { resources }.to raise_error(ArgumentError, 'Namespace is not a root namespace')
end
+ end
- it 'contains only catalog resources for projects in that namespace' do
- is_expected.to contain_exactly(resource, resource_2, resource_3)
+ context 'when the user has access to all projects in the namespace' do
+ context 'when the namespace has no catalog resources' do
+ it { is_expected.to be_empty }
end
- context 'with a sort parameter' do
- subject(:resources) { list.resources(sort: sort) }
+ context 'when the namespace has catalog resources' do
+ let_it_be(:today) { Time.zone.now }
+ let_it_be(:yesterday) { today - 1.day }
+ let_it_be(:tomorrow) { today + 1.day }
- context 'when the sort is name ascending' do
- let_it_be(:sort) { :name_asc }
+ let_it_be(:resource_1) do
+ create(:ci_catalog_resource, project: project_x, latest_released_at: yesterday, created_at: today)
+ end
- it 'contains catalog resources for projects sorted by name ascending' do
- is_expected.to eq([resource_3, resource_2, resource])
- end
+ let_it_be(:resource_2) do
+ create(:ci_catalog_resource, project: project_b, latest_released_at: today, created_at: yesterday)
end
- context 'when the sort is name descending' do
- let_it_be(:sort) { :name_desc }
+ let_it_be(:resource_3) do
+ create(:ci_catalog_resource, project: project_a, latest_released_at: nil, created_at: tomorrow)
+ end
- it 'contains catalog resources for projects sorted by name descending' do
- is_expected.to eq([resource, resource_2, resource_3])
- end
+ let_it_be(:other_namespace_resource) do
+ create(:ci_catalog_resource, project: project_ext, latest_released_at: tomorrow)
end
- context 'when the sort is latest_released_at ascending' do
- let_it_be(:sort) { :latest_released_at_asc }
+ it 'contains only catalog resources for projects in that namespace' do
+ is_expected.to contain_exactly(resource_1, resource_2, resource_3)
+ end
- it 'contains catalog resources sorted by latest_released_at ascending with nulls last' do
- is_expected.to eq([resource, resource_2, resource_3])
+ context 'with a sort parameter' do
+ let(:params) { { namespace: namespace, sort: sort } }
+
+ context 'when the sort is created_at ascending' do
+ let_it_be(:sort) { :created_at_asc }
+
+ it 'contains catalog resources sorted by created_at ascending' do
+ is_expected.to eq([resource_2, resource_1, resource_3])
+ end
+ end
+
+ context 'when the sort is created_at descending' do
+ let_it_be(:sort) { :created_at_desc }
+
+ it 'contains catalog resources sorted by created_at descending' do
+ is_expected.to eq([resource_3, resource_1, resource_2])
+ end
end
- end
- context 'when the sort is latest_released_at descending' do
- let_it_be(:sort) { :latest_released_at_desc }
+ context 'when the sort is name ascending' do
+ let_it_be(:sort) { :name_asc }
- it 'contains catalog resources sorted by latest_released_at descending with nulls last' do
- is_expected.to eq([resource_2, resource, resource_3])
+ it 'contains catalog resources for projects sorted by name ascending' do
+ is_expected.to eq([resource_3, resource_2, resource_1])
+ end
+ end
+
+ context 'when the sort is name descending' do
+ let_it_be(:sort) { :name_desc }
+
+ it 'contains catalog resources for projects sorted by name descending' do
+ is_expected.to eq([resource_1, resource_2, resource_3])
+ end
+ end
+
+ context 'when the sort is latest_released_at ascending' do
+ let_it_be(:sort) { :latest_released_at_asc }
+
+ it 'contains catalog resources sorted by latest_released_at ascending with nulls last' do
+ is_expected.to eq([resource_1, resource_2, resource_3])
+ end
+ end
+
+ context 'when the sort is latest_released_at descending' do
+ let_it_be(:sort) { :latest_released_at_desc }
+
+ it 'contains catalog resources sorted by latest_released_at descending with nulls last' do
+ is_expected.to eq([resource_2, resource_1, resource_3])
+ end
end
end
end
end
- end
- context 'when the user only has access to some projects in the namespace' do
- let!(:resource_1) { create(:ci_catalog_resource, project: project_1) }
- let!(:resource_2) { create(:ci_catalog_resource, project: project_2) }
+ context 'when the user only has access to some projects in the namespace' do
+ let!(:accessible_resource) { create(:ci_catalog_resource, project: project_x) }
+ let!(:inaccessible_resource) { create(:ci_catalog_resource, project: project_noaccess) }
- before do
- project_1.add_developer(user)
- project_2.add_guest(user)
+ it 'only returns catalog resources for projects the user has access to' do
+ is_expected.to contain_exactly(accessible_resource)
+ end
end
- it 'only returns catalog resources for projects the user has access to' do
- is_expected.to contain_exactly(resource_1)
- end
- end
+ context 'when the user does not have access to the namespace' do
+ let!(:project) { create(:project) }
+ let!(:resource) { create(:ci_catalog_resource, project: project) }
- context 'when the user does not have access to the namespace' do
- let!(:resource) { create(:ci_catalog_resource, project: project_1) }
+ let(:namespace) { project.namespace }
- it { is_expected.to be_empty }
+ it { is_expected.to be_empty }
+ end
end
end
end
diff --git a/spec/models/ci/catalog/resource_spec.rb b/spec/models/ci/catalog/resource_spec.rb
index 4ce1433e015..098772b1ea9 100644
--- a/spec/models/ci/catalog/resource_spec.rb
+++ b/spec/models/ci/catalog/resource_spec.rb
@@ -7,10 +7,10 @@ RSpec.describe Ci::Catalog::Resource, feature_category: :pipeline_composition do
let_it_be(:yesterday) { today - 1.day }
let_it_be(:tomorrow) { today + 1.day }
- let_it_be(:project) { create(:project, name: 'A') }
+ let_it_be_with_reload(:project) { create(:project, name: 'A') }
let_it_be(:project_2) { build(:project, name: 'Z') }
- let_it_be(:project_3) { build(:project, name: 'L') }
- let_it_be(:resource) { create(:ci_catalog_resource, project: project, latest_released_at: tomorrow) }
+ let_it_be(:project_3) { build(:project, name: 'L', description: 'Z') }
+ let_it_be_with_reload(:resource) { create(:ci_catalog_resource, project: project, latest_released_at: tomorrow) }
let_it_be(:resource_2) { create(:ci_catalog_resource, project: project_2, latest_released_at: today) }
let_it_be(:resource_3) { create(:ci_catalog_resource, project: project_3, latest_released_at: nil) }
@@ -19,12 +19,16 @@ RSpec.describe Ci::Catalog::Resource, feature_category: :pipeline_composition do
let_it_be(:release3) { create(:release, project: project, released_at: tomorrow) }
it { is_expected.to belong_to(:project) }
- it { is_expected.to have_many(:components).class_name('Ci::Catalog::Resources::Component') }
+
+ it do
+ is_expected.to(
+ have_many(:components).class_name('Ci::Catalog::Resources::Component').with_foreign_key(:catalog_resource_id)
+ )
+ end
+
it { is_expected.to have_many(:versions).class_name('Ci::Catalog::Resources::Version') }
it { is_expected.to delegate_method(:avatar_path).to(:project) }
- it { is_expected.to delegate_method(:description).to(:project) }
- it { is_expected.to delegate_method(:name).to(:project) }
it { is_expected.to delegate_method(:star_count).to(:project) }
it { is_expected.to delegate_method(:forks_count).to(:project) }
@@ -38,6 +42,14 @@ RSpec.describe Ci::Catalog::Resource, feature_category: :pipeline_composition do
end
end
+ describe '.search' do
+ it 'returns catalog resources whose name or description match the search term' do
+ resources = described_class.search('Z')
+
+ expect(resources).to contain_exactly(resource_2, resource_3)
+ end
+ end
+
describe '.order_by_created_at_desc' do
it 'returns catalog resources sorted by descending created at' do
ordered_resources = described_class.order_by_created_at_desc
@@ -46,20 +58,40 @@ RSpec.describe Ci::Catalog::Resource, feature_category: :pipeline_composition do
end
end
+ describe '.order_by_created_at_asc' do
+ it 'returns catalog resources sorted by ascending created at' do
+ ordered_resources = described_class.order_by_created_at_asc
+
+ expect(ordered_resources.to_a).to eq([resource, resource_2, resource_3])
+ end
+ end
+
describe '.order_by_name_desc' do
- it 'returns catalog resources sorted by descending name' do
- ordered_resources = described_class.order_by_name_desc
+ subject(:ordered_resources) { described_class.order_by_name_desc }
+ it 'returns catalog resources sorted by descending name' do
expect(ordered_resources.pluck(:name)).to eq(%w[Z L A])
end
+
+ it 'returns catalog resources sorted by descending name with nulls last' do
+ resource.update!(name: nil)
+
+ expect(ordered_resources.pluck(:name)).to eq(['Z', 'L', nil])
+ end
end
describe '.order_by_name_asc' do
- it 'returns catalog resources sorted by ascending name' do
- ordered_resources = described_class.order_by_name_asc
+ subject(:ordered_resources) { described_class.order_by_name_asc }
+ it 'returns catalog resources sorted by ascending name' do
expect(ordered_resources.pluck(:name)).to eq(%w[A L Z])
end
+
+ it 'returns catalog resources sorted by ascending name with nulls last' do
+ resource.update!(name: nil)
+
+ expect(ordered_resources.pluck(:name)).to eq(['L', 'Z', nil])
+ end
end
describe '.order_by_latest_released_at_desc' do
@@ -78,21 +110,92 @@ RSpec.describe Ci::Catalog::Resource, feature_category: :pipeline_composition do
end
end
- describe '#versions' do
- it 'returns releases ordered by released date descending' do
- expect(resource.versions).to eq([release3, release2, release1])
+ describe '#state' do
+ it 'defaults to draft' do
+ expect(resource.state).to eq('draft')
end
end
- describe '#latest_version' do
- it 'returns the latest release' do
- expect(resource.latest_version).to eq(release3)
+ describe '#publish!' do
+ context 'when the catalog resource is in draft state' do
+ it 'updates the state of the catalog resource to published' do
+ expect(resource.state).to eq('draft')
+
+ resource.publish!
+
+ expect(resource.reload.state).to eq('published')
+ end
+ end
+
+ context 'when a catalog resource already has a published state' do
+ it 'leaves the state as published' do
+ resource.update!(state: 'published')
+
+ resource.publish!
+
+ expect(resource.state).to eq('published')
+ end
end
end
- describe '#state' do
- it 'defaults to draft' do
- expect(resource.state).to eq('draft')
+ describe '#unpublish!' do
+ context 'when the catalog resource is in published state' do
+ it 'updates the state to draft' do
+ resource.update!(state: :published)
+ expect(resource.state).to eq('published')
+
+ resource.unpublish!
+
+ expect(resource.reload.state).to eq('draft')
+ end
+ end
+
+ context 'when the catalog resource is already in draft state' do
+ it 'leaves the state as draft' do
+ expect(resource.state).to eq('draft')
+
+ resource.unpublish!
+
+ expect(resource.reload.state).to eq('draft')
+ end
+ end
+ end
+
+ describe 'sync with project' do
+ shared_examples 'denormalized columns of the catalog resource match the project' do
+ it do
+ expect(resource.name).to eq(project.name)
+ expect(resource.description).to eq(project.description)
+ expect(resource.visibility_level).to eq(project.visibility_level)
+ end
+ end
+
+ context 'when the catalog resource is created' do
+ it_behaves_like 'denormalized columns of the catalog resource match the project'
+ end
+
+ context 'when the project name is updated' do
+ before do
+ project.update!(name: 'My new project name')
+ end
+
+ it_behaves_like 'denormalized columns of the catalog resource match the project'
+ end
+
+ context 'when the project description is updated' do
+ before do
+ project.update!(description: 'My new description')
+ end
+
+ it_behaves_like 'denormalized columns of the catalog resource match the project'
+ end
+
+ context 'when the project visibility_level is updated' do
+ before do
+ project.update!(visibility_level: 10)
+ end
+
+ it_behaves_like 'denormalized columns of the catalog resource match the project'
end
end
end
diff --git a/spec/models/ci/catalog/resources/component_spec.rb b/spec/models/ci/catalog/resources/component_spec.rb
index e8c92ce0788..2ee91175920 100644
--- a/spec/models/ci/catalog/resources/component_spec.rb
+++ b/spec/models/ci/catalog/resources/component_spec.rb
@@ -9,6 +9,23 @@ RSpec.describe Ci::Catalog::Resources::Component, type: :model, feature_category
it { is_expected.to belong_to(:project) }
it { is_expected.to belong_to(:version).class_name('Ci::Catalog::Resources::Version') }
+ it_behaves_like 'a BulkInsertSafe model', described_class do
+ let_it_be(:project) { create(:project, :readme, description: 'project description') }
+ let_it_be(:catalog_resource) { create(:ci_catalog_resource, project: project) }
+ let_it_be(:version) { create(:ci_catalog_resource_version, project: project) }
+
+ let(:valid_items_for_bulk_insertion) do
+ build_list(:ci_catalog_resource_component, 10) do |component|
+ component.catalog_resource = catalog_resource
+ component.version = version
+ component.project = project
+ component.created_at = Time.zone.now
+ end
+ end
+
+ let(:invalid_items_for_bulk_insertion) { [] }
+ end
+
describe 'validations' do
it { is_expected.to validate_presence_of(:catalog_resource) }
it { is_expected.to validate_presence_of(:project) }
diff --git a/spec/models/ci/catalog/resources/version_spec.rb b/spec/models/ci/catalog/resources/version_spec.rb
index e93176e466a..7114d2b6709 100644
--- a/spec/models/ci/catalog/resources/version_spec.rb
+++ b/spec/models/ci/catalog/resources/version_spec.rb
@@ -3,14 +3,105 @@
require 'spec_helper'
RSpec.describe Ci::Catalog::Resources::Version, type: :model, feature_category: :pipeline_composition do
+ include_context 'when there are catalog resources with versions'
+
it { is_expected.to belong_to(:release) }
it { is_expected.to belong_to(:catalog_resource).class_name('Ci::Catalog::Resource') }
it { is_expected.to belong_to(:project) }
it { is_expected.to have_many(:components).class_name('Ci::Catalog::Resources::Component') }
+ it { is_expected.to delegate_method(:name).to(:release) }
+ it { is_expected.to delegate_method(:description).to(:release) }
+ it { is_expected.to delegate_method(:tag).to(:release) }
+ it { is_expected.to delegate_method(:sha).to(:release) }
+ it { is_expected.to delegate_method(:released_at).to(:release) }
+ it { is_expected.to delegate_method(:author_id).to(:release) }
+
describe 'validations' do
it { is_expected.to validate_presence_of(:release) }
it { is_expected.to validate_presence_of(:catalog_resource) }
it { is_expected.to validate_presence_of(:project) }
end
+
+ describe '.for_catalog resources' do
+ it 'returns versions for the given catalog resources' do
+ versions = described_class.for_catalog_resources([resource1, resource2])
+
+ expect(versions).to match_array([v1_0, v1_1, v2_0, v2_1])
+ end
+ end
+
+ describe '.order_by_created_at_asc' do
+ it 'returns versions ordered by created_at ascending' do
+ versions = described_class.order_by_created_at_asc
+
+ expect(versions).to eq([v2_1, v2_0, v1_1, v1_0])
+ end
+ end
+
+ describe '.order_by_created_at_desc' do
+ it 'returns versions ordered by created_at descending' do
+ versions = described_class.order_by_created_at_desc
+
+ expect(versions).to eq([v1_0, v1_1, v2_0, v2_1])
+ end
+ end
+
+ describe '.order_by_released_at_asc' do
+ it 'returns versions ordered by released_at ascending' do
+ versions = described_class.order_by_released_at_asc
+
+ expect(versions).to eq([v1_0, v1_1, v2_0, v2_1])
+ end
+ end
+
+ describe '.order_by_released_at_desc' do
+ it 'returns versions ordered by released_at descending' do
+ versions = described_class.order_by_released_at_desc
+
+ expect(versions).to eq([v2_1, v2_0, v1_1, v1_0])
+ end
+ end
+
+ describe '.latest' do
+ subject { described_class.latest }
+
+ it 'returns the latest version by released date' do
+ is_expected.to eq(v2_1)
+ end
+
+ context 'when there are no versions' do
+ it 'returns nil' do
+ resource1.versions.delete_all(:delete_all)
+ resource2.versions.delete_all(:delete_all)
+
+ is_expected.to be_nil
+ end
+ end
+ end
+
+ describe '.latest_for_catalog resources' do
+ subject { described_class.latest_for_catalog_resources([resource1, resource2]) }
+
+ it 'returns the latest version for each catalog resource' do
+ is_expected.to match_array([v1_1, v2_1])
+ end
+
+ context 'when one catalog resource does not have versions' do
+ it 'returns the latest version of only the catalog resource with versions' do
+ resource1.versions.delete_all(:delete_all)
+
+ is_expected.to match_array([v2_1])
+ end
+ end
+
+ context 'when no catalog resource has versions' do
+ it 'returns empty response' do
+ resource1.versions.delete_all(:delete_all)
+ resource2.versions.delete_all(:delete_all)
+
+ is_expected.to be_empty
+ end
+ end
+ end
end
diff --git a/spec/models/ci/job_artifact_spec.rb b/spec/models/ci/job_artifact_spec.rb
index 498af80dbb6..48d46824c11 100644
--- a/spec/models/ci/job_artifact_spec.rb
+++ b/spec/models/ci/job_artifact_spec.rb
@@ -207,7 +207,7 @@ RSpec.describe Ci::JobArtifact, feature_category: :build_artifacts do
subject { described_class.associated_file_types_for(file_type) }
where(:file_type, :result) do
- 'codequality' | %w(codequality)
+ 'codequality' | %w[codequality]
'quality' | nil
end
diff --git a/spec/models/ci/job_token/scope_spec.rb b/spec/models/ci/job_token/scope_spec.rb
index 7aa861a3dab..d41286f5a45 100644
--- a/spec/models/ci/job_token/scope_spec.rb
+++ b/spec/models/ci/job_token/scope_spec.rb
@@ -88,24 +88,6 @@ RSpec.describe Ci::JobToken::Scope, feature_category: :continuous_integration, f
end
end
- RSpec.shared_examples 'enforces outbound scope only' do
- include_context 'with accessible and inaccessible projects'
-
- where(:accessed_project, :result) do
- ref(:current_project) | true
- ref(:inbound_allowlist_project) | false
- ref(:unscoped_project1) | false
- ref(:unscoped_project2) | false
- ref(:outbound_allowlist_project) | true
- ref(:inbound_accessible_project) | false
- ref(:fully_accessible_project) | true
- end
-
- with_them do
- it { is_expected.to eq(result) }
- end
- end
-
describe 'accessible?' do
subject { scope.accessible?(accessed_project) }
@@ -121,6 +103,7 @@ RSpec.describe Ci::JobToken::Scope, feature_category: :continuous_integration, f
ref(:outbound_allowlist_project) | false
ref(:inbound_accessible_project) | false
ref(:fully_accessible_project) | true
+ ref(:unscoped_public_project) | false
end
with_them do
@@ -147,6 +130,7 @@ RSpec.describe Ci::JobToken::Scope, feature_category: :continuous_integration, f
ref(:outbound_allowlist_project) | false
ref(:inbound_accessible_project) | true
ref(:fully_accessible_project) | true
+ ref(:unscoped_public_project) | false
end
with_them do
@@ -160,7 +144,34 @@ RSpec.describe Ci::JobToken::Scope, feature_category: :continuous_integration, f
current_project.update!(ci_outbound_job_token_scope_enabled: true)
end
- include_examples 'enforces outbound scope only'
+ include_context 'with accessible and inaccessible projects'
+
+ where(:accessed_project, :result) do
+ ref(:current_project) | true
+ ref(:inbound_allowlist_project) | false
+ ref(:unscoped_project1) | false
+ ref(:unscoped_project2) | false
+ ref(:outbound_allowlist_project) | true
+ ref(:inbound_accessible_project) | false
+ ref(:fully_accessible_project) | true
+ ref(:unscoped_public_project) | true
+ end
+
+ with_them do
+ it { is_expected.to eq(result) }
+ end
+
+ context "with FF restrict_ci_job_token_for_public_and_internal_projects disabled" do
+ before do
+ stub_feature_flags(restrict_ci_job_token_for_public_and_internal_projects: false)
+ end
+
+ let(:accessed_project) { unscoped_public_project }
+
+ it "restricts public and internal outbound projects not in allowlist" do
+ is_expected.to eq(false)
+ end
+ end
end
end
end
diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb
index 887ec48ec8f..9696ba7b3ee 100644
--- a/spec/models/ci/pipeline_spec.rb
+++ b/spec/models/ci/pipeline_spec.rb
@@ -157,6 +157,106 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep, feature_category:
end
end
+ describe 'unlocking pipelines based on state transition' do
+ let(:ci_ref) { create(:ci_ref) }
+ let(:unlock_previous_pipelines_worker_spy) { class_spy(::Ci::Refs::UnlockPreviousPipelinesWorker) }
+
+ before do
+ stub_const('Ci::Refs::UnlockPreviousPipelinesWorker', unlock_previous_pipelines_worker_spy)
+ stub_feature_flags(ci_stop_unlock_pipelines: false)
+ end
+
+ shared_examples 'not unlocking pipelines' do |event:|
+ context "on #{event}" do
+ let(:pipeline) { create(:ci_pipeline, ci_ref_id: ci_ref.id, locked: :artifacts_locked) }
+
+ it 'does not unlock previous pipelines' do
+ pipeline.fire_events!(event)
+
+ expect(unlock_previous_pipelines_worker_spy).not_to have_received(:perform_async)
+ end
+ end
+ end
+
+ shared_examples 'unlocking pipelines' do |event:|
+ context "on #{event}" do
+ before do
+ pipeline.fire_events!(event)
+ end
+
+ let(:pipeline) { create(:ci_pipeline, ci_ref_id: ci_ref.id, locked: :artifacts_locked) }
+
+ it 'unlocks previous pipelines' do
+ expect(unlock_previous_pipelines_worker_spy).to have_received(:perform_async).with(ci_ref.id)
+ end
+ end
+ end
+
+ context 'when transitioning to unlockable states' do
+ before do
+ pipeline.run
+ end
+
+ it_behaves_like 'unlocking pipelines', event: :succeed
+ it_behaves_like 'unlocking pipelines', event: :drop
+ it_behaves_like 'unlocking pipelines', event: :skip
+ it_behaves_like 'unlocking pipelines', event: :cancel
+ it_behaves_like 'unlocking pipelines', event: :block
+
+ context 'and ci_stop_unlock_pipelines is enabled' do
+ before do
+ stub_feature_flags(ci_stop_unlock_pipelines: true)
+ end
+
+ it_behaves_like 'not unlocking pipelines', event: :succeed
+ it_behaves_like 'not unlocking pipelines', event: :drop
+ it_behaves_like 'not unlocking pipelines', event: :skip
+ it_behaves_like 'not unlocking pipelines', event: :cancel
+ it_behaves_like 'not unlocking pipelines', event: :block
+ end
+
+ context 'and ci_unlock_non_successful_pipelines is disabled' do
+ before do
+ stub_feature_flags(ci_unlock_non_successful_pipelines: false)
+ end
+
+ it_behaves_like 'unlocking pipelines', event: :succeed
+ it_behaves_like 'not unlocking pipelines', event: :drop
+ it_behaves_like 'not unlocking pipelines', event: :skip
+ it_behaves_like 'not unlocking pipelines', event: :cancel
+ it_behaves_like 'not unlocking pipelines', event: :block
+
+ context 'and ci_stop_unlock_pipelines is enabled' do
+ before do
+ stub_feature_flags(ci_stop_unlock_pipelines: true)
+ end
+
+ it_behaves_like 'not unlocking pipelines', event: :succeed
+ it_behaves_like 'not unlocking pipelines', event: :drop
+ it_behaves_like 'not unlocking pipelines', event: :skip
+ it_behaves_like 'not unlocking pipelines', event: :cancel
+ it_behaves_like 'not unlocking pipelines', event: :block
+ end
+ end
+ end
+
+ context 'when transitioning to a non-unlockable state' do
+ before do
+ pipeline.enqueue
+ end
+
+ it_behaves_like 'not unlocking pipelines', event: :run
+
+ context 'and ci_unlock_non_successful_pipelines is disabled' do
+ before do
+ stub_feature_flags(ci_unlock_non_successful_pipelines: false)
+ end
+
+ it_behaves_like 'not unlocking pipelines', event: :run
+ end
+ end
+ end
+
describe 'pipeline age metric' do
let_it_be(:pipeline) { create(:ci_empty_pipeline, :created) }
@@ -220,6 +320,34 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep, feature_category:
end
end
+ describe '.with_unlockable_status' do
+ let_it_be(:project) { create(:project) }
+
+ let!(:pipeline) { create(:ci_pipeline, project: project, status: status) }
+
+ subject(:result) { described_class.with_unlockable_status }
+
+ described_class::UNLOCKABLE_STATUSES.map(&:to_s).each do |s|
+ context "when pipeline status is #{s}" do
+ let(:status) { s }
+
+ it 'includes the pipeline in the result' do
+ expect(result).to include(pipeline)
+ end
+ end
+ end
+
+ (Ci::HasStatus::AVAILABLE_STATUSES - described_class::UNLOCKABLE_STATUSES.map(&:to_s)).each do |s|
+ context "when pipeline status is #{s}" do
+ let(:status) { s }
+
+ it 'does excludes the pipeline in the result' do
+ expect(result).not_to include(pipeline)
+ end
+ end
+ end
+ end
+
describe '.processables' do
let_it_be(:pipeline) { create(:ci_empty_pipeline, :created) }
@@ -231,7 +359,7 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep, feature_category:
end
it 'has an association with processable CI/CD entities' do
- pipeline.processables.pluck('name').yield_self do |processables|
+ pipeline.processables.pluck('name').then do |processables|
expect(processables).to match_array %w[build bridge]
end
end
@@ -1303,7 +1431,7 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep, feature_category:
describe '#stages_names' do
it 'returns a valid names of stages' do
- expect(pipeline.stages_names).to eq(%w(build test deploy))
+ expect(pipeline.stages_names).to eq(%w[build test deploy])
end
end
end
@@ -1900,11 +2028,23 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep, feature_category:
end
end
- context 'when only_allow_merge_if_pipeline_succeeds? returns false' do
+ context 'when only_allow_merge_if_pipeline_succeeds? returns false and widget_pipeline_pass_subscription_update disabled' do
let(:only_allow_merge_if_pipeline_succeeds?) { false }
+ before do
+ stub_feature_flags(widget_pipeline_pass_subscription_update: false)
+ end
+
it_behaves_like 'state transition not triggering GraphQL subscription mergeRequestMergeStatusUpdated'
end
+
+ context 'when only_allow_merge_if_pipeline_succeeds? returns false and widget_pipeline_pass_subscription_update enabled' do
+ let(:only_allow_merge_if_pipeline_succeeds?) { false }
+
+ it_behaves_like 'triggers GraphQL subscription mergeRequestMergeStatusUpdated' do
+ let(:action) { pipeline.succeed }
+ end
+ end
end
context 'when pipeline has merge requests' do
@@ -2640,7 +2780,7 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep, feature_category:
subject(:latest_successful_for_refs) { described_class.latest_successful_for_refs(refs) }
context 'when refs are specified' do
- let(:refs) { %w(first_ref second_ref third_ref) }
+ let(:refs) { %w[first_ref second_ref third_ref] }
before do
create(:ci_empty_pipeline, id: 1001, status: :success, ref: 'first_ref', sha: 'sha')
@@ -2819,7 +2959,7 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep, feature_category:
subject { described_class.bridgeable_statuses }
it { is_expected.to be_an(Array) }
- it { is_expected.not_to include('created', 'waiting_for_resource', 'preparing', 'pending') }
+ it { is_expected.to contain_exactly('running', 'success', 'failed', 'canceled', 'skipped', 'manual', 'scheduled') }
end
describe '#status', :sidekiq_inline do
@@ -3176,6 +3316,7 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep, feature_category:
%i[
enqueue
request_resource
+ wait_for_callback
prepare
run
skip
@@ -5578,25 +5719,4 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep, feature_category:
end
end
end
-
- describe '#reduced_build_attributes_list_for_rules?' do
- subject { pipeline.reduced_build_attributes_list_for_rules? }
-
- let(:pipeline) { build_stubbed(:ci_pipeline, project: project, user: user) }
-
- it { is_expected.to be_truthy }
-
- it 'memoizes the result' do
- expect { subject }
- .to change { pipeline.strong_memoized?(:reduced_build_attributes_list_for_rules?) }
- end
-
- context 'with the FF disabled' do
- before do
- stub_feature_flags(reduced_build_attributes_list_for_rules: false)
- end
-
- it { is_expected.to be_falsey }
- end
- end
end
diff --git a/spec/models/ci/ref_spec.rb b/spec/models/ci/ref_spec.rb
index 75071a17fa9..2727c7701b8 100644
--- a/spec/models/ci/ref_spec.rb
+++ b/spec/models/ci/ref_spec.rb
@@ -7,61 +7,6 @@ RSpec.describe Ci::Ref, feature_category: :continuous_integration do
it { is_expected.to belong_to(:project) }
- describe 'state machine transitions' do
- context 'unlock artifacts transition' do
- let(:ci_ref) { create(:ci_ref) }
- let(:unlock_previous_pipelines_worker_spy) { class_spy(::Ci::Refs::UnlockPreviousPipelinesWorker) }
-
- before do
- stub_const('Ci::Refs::UnlockPreviousPipelinesWorker', unlock_previous_pipelines_worker_spy)
- end
-
- context 'pipeline is locked' do
- let!(:pipeline) { create(:ci_pipeline, ci_ref_id: ci_ref.id, locked: :artifacts_locked) }
-
- where(:initial_state, :action, :count) do
- :unknown | :succeed! | 1
- :unknown | :do_fail! | 0
- :success | :succeed! | 1
- :success | :do_fail! | 0
- :failed | :succeed! | 1
- :failed | :do_fail! | 0
- :fixed | :succeed! | 1
- :fixed | :do_fail! | 0
- :broken | :succeed! | 1
- :broken | :do_fail! | 0
- :still_failing | :succeed | 1
- :still_failing | :do_fail | 0
- end
-
- with_them do
- context "when transitioning states" do
- before do
- status_value = Ci::Ref.state_machines[:status].states[initial_state].value
- ci_ref.update!(status: status_value)
- end
-
- it 'calls pipeline complete unlock artifacts service' do
- ci_ref.send(action)
-
- expect(unlock_previous_pipelines_worker_spy).to have_received(:perform_async).exactly(count).times
- end
- end
- end
- end
-
- context 'pipeline is unlocked' do
- let!(:pipeline) { create(:ci_pipeline, ci_ref_id: ci_ref.id, locked: :unlocked) }
-
- it 'does not unlock pipelines' do
- ci_ref.succeed!
-
- expect(unlock_previous_pipelines_worker_spy).not_to have_received(:perform_async)
- end
- end
- end
- end
-
describe '.ensure_for' do
let_it_be(:project) { create(:project, :repository) }
@@ -241,4 +186,117 @@ RSpec.describe Ci::Ref, feature_category: :continuous_integration do
let!(:model) { create(:ci_ref, project: parent) }
end
end
+
+ describe '#last_successful_ci_source_pipeline' do
+ let_it_be(:ci_ref) { create(:ci_ref) }
+
+ let(:ci_source) { Enums::Ci::Pipeline.sources[:push] }
+ let(:dangling_source) { Enums::Ci::Pipeline.sources[:parent_pipeline] }
+
+ subject(:result) { ci_ref.last_successful_ci_source_pipeline }
+
+ context 'when there are no successful CI source pipelines' do
+ let!(:running_ci_source) { create(:ci_pipeline, :running, ci_ref: ci_ref, source: ci_source) }
+ let!(:successful_dangling_source) { create(:ci_pipeline, :success, ci_ref: ci_ref, source: dangling_source) }
+
+ it { is_expected.to be_nil }
+ end
+
+ context 'when there are successful CI source pipelines' do
+ context 'and the latest pipeline is a successful CI source pipeline' do
+ let!(:failed_ci_source) { create(:ci_pipeline, :failed, ci_ref: ci_ref, source: ci_source) }
+ let!(:successful_dangling_source) { create(:ci_pipeline, :success, ci_ref: ci_ref, source: dangling_source, child_of: failed_ci_source) }
+ let!(:successful_ci_source) { create(:ci_pipeline, :success, ci_ref: ci_ref, source: ci_source) }
+
+ it 'returns the last successful CI source pipeline' do
+ expect(result).to eq(successful_ci_source)
+ end
+ end
+
+ context 'and there is a newer successful dangling source pipeline than the successful CI source pipelines' do
+ let!(:successful_ci_source_1) { create(:ci_pipeline, :success, ci_ref: ci_ref, source: ci_source) }
+ let!(:successful_ci_source_2) { create(:ci_pipeline, :success, ci_ref: ci_ref, source: ci_source) }
+ let!(:failed_ci_source) { create(:ci_pipeline, :failed, ci_ref: ci_ref, source: ci_source) }
+ let!(:successful_dangling_source) { create(:ci_pipeline, :success, ci_ref: ci_ref, source: dangling_source, child_of: failed_ci_source) }
+
+ it 'returns the last successful CI source pipeline' do
+ expect(result).to eq(successful_ci_source_2)
+ end
+
+ context 'and the newer successful dangling source is a child of a successful CI source pipeline' do
+ let!(:parent_ci_source) { create(:ci_pipeline, :success, ci_ref: ci_ref, source: ci_source) }
+ let!(:successful_child_source) { create(:ci_pipeline, :success, ci_ref: ci_ref, source: dangling_source, child_of: parent_ci_source) }
+
+ it 'returns the parent pipeline instead' do
+ expect(result).to eq(parent_ci_source)
+ end
+ end
+ end
+ end
+ end
+
+ describe '#last_unlockable_ci_source_pipeline' do
+ let(:ci_source) { Enums::Ci::Pipeline.sources[:push] }
+ let(:dangling_source) { Enums::Ci::Pipeline.sources[:parent_pipeline] }
+
+ let_it_be(:project) { create(:project) }
+ let_it_be(:ci_ref) { create(:ci_ref, project: project) }
+
+ subject(:result) { ci_ref.last_unlockable_ci_source_pipeline }
+
+ context 'when there are unlockable pipelines in the ref' do
+ context 'and the last CI source pipeline in the ref is unlockable' do
+ let!(:unlockable_ci_source_1) { create(:ci_pipeline, :success, project: project, ci_ref: ci_ref, source: ci_source) }
+ let!(:unlockable_ci_source_2) { create(:ci_pipeline, :blocked, project: project, ci_ref: ci_ref, source: ci_source) }
+
+ it 'returns the CI source pipeline' do
+ expect(result).to eq(unlockable_ci_source_2)
+ end
+
+ context 'and it has unlockable child pipelines' do
+ let!(:child) { create(:ci_pipeline, :success, project: project, ci_ref: ci_ref, source: dangling_source, child_of: unlockable_ci_source_2) }
+ let!(:child_2) { create(:ci_pipeline, :success, project: project, ci_ref: ci_ref, source: dangling_source, child_of: unlockable_ci_source_2) }
+
+ it 'returns the parent CI source pipeline' do
+ expect(result).to eq(unlockable_ci_source_2)
+ end
+ end
+
+ context 'and it has a non-unlockable child pipeline' do
+ let!(:child) { create(:ci_pipeline, :running, project: project, ci_ref: ci_ref, source: dangling_source, child_of: unlockable_ci_source_2) }
+
+ it 'returns the parent CI source pipeline' do
+ expect(result).to eq(unlockable_ci_source_2)
+ end
+ end
+ end
+
+ context 'and the last CI source pipeline in the ref is not unlockable' do
+ let!(:unlockable_ci_source) { create(:ci_pipeline, :skipped, project: project, ci_ref: ci_ref, source: ci_source) }
+ let!(:unlockable_dangling_source) { create(:ci_pipeline, :success, project: project, ci_ref: ci_ref, source: dangling_source, child_of: unlockable_ci_source) }
+ let!(:non_unlockable_ci_source) { create(:ci_pipeline, :running, project: project, ci_ref: ci_ref, source: ci_source) }
+ let!(:non_unlockable_ci_source_2) { create(:ci_pipeline, :running, project: project, ci_ref: ci_ref, source: ci_source) }
+
+ it 'returns the last unlockable CI source pipeline before it' do
+ expect(result).to eq(unlockable_ci_source)
+ end
+
+ context 'and it has unlockable child pipelines' do
+ let!(:child) { create(:ci_pipeline, :success, project: project, ci_ref: ci_ref, source: dangling_source, child_of: non_unlockable_ci_source) }
+ let!(:child_2) { create(:ci_pipeline, :success, project: project, ci_ref: ci_ref, source: dangling_source, child_of: non_unlockable_ci_source) }
+
+ it 'returns the last unlockable CI source pipeline before it' do
+ expect(result).to eq(unlockable_ci_source)
+ end
+ end
+ end
+ end
+
+ context 'when there are no unlockable pipelines in the ref' do
+ let!(:non_unlockable_pipeline) { create(:ci_pipeline, :running, project: project, ci_ref: ci_ref, source: ci_source) }
+ let!(:pipeline_from_another_ref) { create(:ci_pipeline, :success, source: ci_source) }
+
+ it { is_expected.to be_nil }
+ end
+ end
end
diff --git a/spec/models/ci/resource_group_spec.rb b/spec/models/ci/resource_group_spec.rb
index 6d518d5c874..9e98cc884de 100644
--- a/spec/models/ci/resource_group_spec.rb
+++ b/spec/models/ci/resource_group_spec.rb
@@ -118,7 +118,7 @@ RSpec.describe Ci::ResourceGroup do
let!(:resource_group) { create(:ci_resource_group, process_mode: process_mode, project: project) }
- Ci::HasStatus::STATUSES_ENUM.keys.each do |status| # rubocop:diable RSpec/UselessDynamicDefinition
+ Ci::HasStatus::STATUSES_ENUM.each_key do |status| # rubocop:disable RSpec/UselessDynamicDefinition -- `status` used in `let`
let!("build_1_#{status}") { create(:ci_build, pipeline: pipeline_1, status: status, resource_group: resource_group) }
let!("build_2_#{status}") { create(:ci_build, pipeline: pipeline_2, status: status, resource_group: resource_group) }
end
diff --git a/spec/models/ci/runner_manager_spec.rb b/spec/models/ci/runner_manager_spec.rb
index bc1d1a0cc49..01275ffd31c 100644
--- a/spec/models/ci/runner_manager_spec.rb
+++ b/spec/models/ci/runner_manager_spec.rb
@@ -413,4 +413,68 @@ RSpec.describe Ci::RunnerManager, feature_category: :runner_fleet, type: :model
end
end
end
+
+ describe '.with_version_prefix' do
+ subject { described_class.with_version_prefix(version_prefix) }
+
+ let_it_be(:runner_manager1) { create(:ci_runner_machine, version: '15.11.0') }
+ let_it_be(:runner_manager2) { create(:ci_runner_machine, version: '15.9.0') }
+ let_it_be(:runner_manager3) { create(:ci_runner_machine, version: '15.11.5') }
+
+ context 'with a prefix string of "15."' do
+ let(:version_prefix) { "15." }
+
+ it 'returns runner managers' do
+ is_expected.to contain_exactly(runner_manager1, runner_manager2, runner_manager3)
+ end
+ end
+
+ context 'with a prefix string of "15"' do
+ let(:version_prefix) { "15" }
+
+ it 'returns runner managers' do
+ is_expected.to contain_exactly(runner_manager1, runner_manager2, runner_manager3)
+ end
+ end
+
+ context 'with a prefix string of "15.11."' do
+ let(:version_prefix) { "15.11." }
+
+ it 'returns runner managers' do
+ is_expected.to contain_exactly(runner_manager1, runner_manager3)
+ end
+ end
+
+ context 'with a prefix string of "15.11"' do
+ let(:version_prefix) { "15.11" }
+
+ it 'returns runner managers' do
+ is_expected.to contain_exactly(runner_manager1, runner_manager3)
+ end
+ end
+
+ context 'with a prefix string of "15.9"' do
+ let(:version_prefix) { "15.9" }
+
+ it 'returns runner managers' do
+ is_expected.to contain_exactly(runner_manager2)
+ end
+ end
+
+ context 'with a prefix string of "15.11.5"' do
+ let(:version_prefix) { "15.11.5" }
+
+ it 'returns runner managers' do
+ is_expected.to contain_exactly(runner_manager3)
+ end
+ end
+
+ context 'with a malformed prefix of "V2"' do
+ let(:version_prefix) { "V2" }
+
+ it 'returns no runner managers' do
+ is_expected.to be_empty
+ end
+ end
+ end
end
diff --git a/spec/models/ci/runner_spec.rb b/spec/models/ci/runner_spec.rb
index 3a3ef072b28..bb9ac084ed6 100644
--- a/spec/models/ci/runner_spec.rb
+++ b/spec/models/ci/runner_spec.rb
@@ -524,6 +524,39 @@ RSpec.describe Ci::Runner, type: :model, feature_category: :runner do
end
end
+ describe '.with_creator_id' do
+ subject { described_class.with_creator_id('1') }
+
+ let_it_be(:runner1) { create(:ci_runner, creator_id: 2) }
+ let_it_be(:runner2) { create(:ci_runner, creator_id: 1) }
+ let_it_be(:runner3) { create(:ci_runner, creator_id: 1) }
+ let_it_be(:runner4) { create(:ci_runner, creator_id: nil) }
+
+ it 'returns runners with creator_id \'1\'' do
+ is_expected.to contain_exactly(runner2, runner3)
+ end
+ end
+
+ describe '.with_version_prefix' do
+ subject { described_class.with_version_prefix('15.11.') }
+
+ let_it_be(:runner1) { create(:ci_runner) }
+ let_it_be(:runner2) { create(:ci_runner) }
+ let_it_be(:runner3) { create(:ci_runner) }
+
+ before_all do
+ create(:ci_runner_machine, runner: runner1, version: '15.11.0')
+ create(:ci_runner_machine, runner: runner2, version: '15.9.0')
+ create(:ci_runner_machine, runner: runner3, version: '15.9.0')
+ # Add another runner_machine to runner3 to ensure edge case is handled (searching multiple machines in a single runner)
+ create(:ci_runner_machine, runner: runner3, version: '15.11.5')
+ end
+
+ it 'returns runners containing runner managers with versions starting with 15.11.' do
+ is_expected.to contain_exactly(runner1, runner3)
+ end
+ end
+
describe '.stale', :freeze_time do
subject { described_class.stale }
@@ -739,7 +772,7 @@ RSpec.describe Ci::Runner, type: :model, feature_category: :runner do
end
context 'when runner has tags' do
- let(:tag_list) { %w(bb cc) }
+ let(:tag_list) { %w[bb cc] }
shared_examples 'tagged build picker' do
it 'can handle build with matching tags' do
@@ -1026,7 +1059,7 @@ RSpec.describe Ci::Runner, type: :model, feature_category: :runner do
end
def value_in_queues
- Gitlab::Redis::SharedState.with do |redis|
+ Gitlab::Redis::Workhorse.with do |redis|
runner_queue_key = runner.send(:runner_queue_key)
redis.get(runner_queue_key)
end
diff --git a/spec/models/ci/stage_spec.rb b/spec/models/ci/stage_spec.rb
index 1be50083cd4..4951f57fe6f 100644
--- a/spec/models/ci/stage_spec.rb
+++ b/spec/models/ci/stage_spec.rb
@@ -166,6 +166,18 @@ RSpec.describe Ci::Stage, :models, feature_category: :continuous_integration do
end
end
+ context 'when build is waiting for callback' do
+ before do
+ create(:ci_build, :waiting_for_callback, stage_id: stage.id)
+ end
+
+ it 'updates status to waiting for callback' do
+ expect { stage.update_legacy_status }
+ .to change { stage.reload.status }
+ .to 'waiting_for_callback'
+ end
+ end
+
context 'when stage is skipped because is empty' do
it 'updates status to skipped' do
expect { stage.update_legacy_status }
diff --git a/spec/models/clusters/agent_spec.rb b/spec/models/clusters/agent_spec.rb
index 6201b7b1861..062d5062658 100644
--- a/spec/models/clusters/agent_spec.rb
+++ b/spec/models/clusters/agent_spec.rb
@@ -21,7 +21,7 @@ RSpec.describe Clusters::Agent, feature_category: :deployment_management do
describe 'scopes' do
describe '.ordered_by_name' do
- let(:names) { %w(agent-d agent-b agent-a agent-c) }
+ let(:names) { %w[agent-d agent-b agent-a agent-c] }
subject { described_class.ordered_by_name }
diff --git a/spec/models/clusters/platforms/kubernetes_spec.rb b/spec/models/clusters/platforms/kubernetes_spec.rb
index c32abaf50f5..79a81977611 100644
--- a/spec/models/clusters/platforms/kubernetes_spec.rb
+++ b/spec/models/clusters/platforms/kubernetes_spec.rb
@@ -10,7 +10,7 @@ RSpec.describe Clusters::Platforms::Kubernetes do
it { is_expected.to be_kind_of(Gitlab::Kubernetes) }
it { is_expected.to respond_to :ca_pem }
- it { is_expected.to validate_exclusion_of(:namespace).in_array(%w(gitlab-managed-apps)) }
+ it { is_expected.to validate_exclusion_of(:namespace).in_array(%w[gitlab-managed-apps]) }
it { is_expected.to validate_presence_of(:api_url) }
it { is_expected.to validate_presence_of(:token) }
diff --git a/spec/models/commit_range_spec.rb b/spec/models/commit_range_spec.rb
index 334833e884b..fe8c28d7251 100644
--- a/spec/models/commit_range_spec.rb
+++ b/spec/models/commit_range_spec.rb
@@ -77,7 +77,7 @@ RSpec.describe CommitRange do
describe '#to_param' do
it 'includes the correct keys' do
- expect(range.to_param.keys).to eq %i(from to)
+ expect(range.to_param.keys).to eq %i[from to]
end
it 'includes the correct values for a three-dot range' do
diff --git a/spec/models/commit_spec.rb b/spec/models/commit_spec.rb
index 7ab43611108..e873a59b54a 100644
--- a/spec/models/commit_spec.rb
+++ b/spec/models/commit_spec.rb
@@ -23,13 +23,13 @@ RSpec.describe Commit do
shared_examples '.lazy checks' do
context 'when the commits are found' do
let(:oids) do
- %w(
+ %w[
498214de67004b1da3d820901307bed2a68a8ef6
c642fe9b8b9f28f9225d7ea953fe14e74748d53b
6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
048721d90c449b244b7b4c53a9186b04330174ec
281d3a76f31c812dbf48abce82ccf6860adedd81
- )
+ ]
end
subject { oids.map { |oid| described_class.lazy(container, oid) } }
@@ -707,16 +707,6 @@ eos
it_behaves_like "#uri_type"
end
- describe '#uri_type with Rugged enabled', :enable_rugged do
- it 'calls out to the Rugged implementation' do
- allow_any_instance_of(Rugged::Tree).to receive(:path).with('files/html').and_call_original
-
- commit.uri_type('files/html')
- end
-
- it_behaves_like '#uri_type'
- end
-
describe '.diff_max_files' do
subject(:diff_max_files) { described_class.diff_max_files }
diff --git a/spec/models/commit_status_spec.rb b/spec/models/commit_status_spec.rb
index e9257b08bca..618dd3a3f77 100644
--- a/spec/models/commit_status_spec.rb
+++ b/spec/models/commit_status_spec.rb
@@ -27,7 +27,7 @@ RSpec.describe CommitStatus, feature_category: :continuous_integration do
it { is_expected.to belong_to(:auto_canceled_by) }
it { is_expected.to validate_presence_of(:name) }
- it { is_expected.to validate_inclusion_of(:status).in_array(%w(pending running failed success canceled)) }
+ it { is_expected.to validate_inclusion_of(:status).in_array(%w[pending running failed success canceled]) }
it { is_expected.to validate_length_of(:stage).is_at_most(255) }
it { is_expected.to validate_length_of(:ref).is_at_most(255) }
@@ -44,24 +44,6 @@ RSpec.describe CommitStatus, feature_category: :continuous_integration do
it { is_expected.not_to be_retried }
it { expect(described_class.primary_key).to eq('id') }
- describe '.switch_table_names' do
- before do
- stub_env('USE_CI_BUILDS_ROUTING_TABLE', flag_value)
- end
-
- context 'with the env flag disabled' do
- let(:flag_value) { 'false' }
-
- it { expect(described_class.switch_table_names).to eq(:ci_builds) }
- end
-
- context 'with the env flag enabled' do
- let(:flag_value) { 'true' }
-
- it { expect(described_class.switch_table_names).to eq(:p_ci_builds) }
- end
- end
-
describe '#author' do
subject { commit_status.author }
@@ -174,7 +156,7 @@ RSpec.describe CommitStatus, feature_category: :continuous_integration do
describe '.cancelable' do
subject { described_class.cancelable }
- %i[running pending waiting_for_resource preparing created scheduled].each do |status|
+ %i[running pending waiting_for_resource waiting_for_callback preparing created scheduled].each do |status|
context "when #{status} commit status" do
let!(:commit_status) { create(:commit_status, status, pipeline: pipeline) }
diff --git a/spec/models/compare_spec.rb b/spec/models/compare_spec.rb
index 2206ed7bfe8..78610d002b2 100644
--- a/spec/models/compare_spec.rb
+++ b/spec/models/compare_spec.rb
@@ -129,13 +129,13 @@ RSpec.describe Compare, feature_category: :source_code_management do
it 'returns affected file paths, without duplication' do
expect(subject.modified_paths).to contain_exactly(
- *%w{
+ *%w[
foo/for_move.txt
foo/bar/for_move.txt
foo/for_create.txt
foo/for_delete.txt
foo/for_edit.txt
- })
+ ])
end
end
diff --git a/spec/models/concerns/awardable_spec.rb b/spec/models/concerns/awardable_spec.rb
index fcd0d0c05f4..828b75aceb0 100644
--- a/spec/models/concerns/awardable_spec.rb
+++ b/spec/models/concerns/awardable_spec.rb
@@ -102,7 +102,7 @@ RSpec.describe Awardable do
end
it "includes unused thumbs buttons by default" do
- expect(note_without_downvote.grouped_awards.keys.sort).to eq %w(thumbsdown thumbsup)
+ expect(note_without_downvote.grouped_awards.keys.sort).to eq %w[thumbsdown thumbsup]
end
it "doesn't include unused thumbs buttons when disabled in project" do
@@ -114,7 +114,7 @@ RSpec.describe Awardable do
it "includes unused thumbs buttons when enabled in project" do
note_without_downvote.project.show_default_award_emojis = true
- expect(note_without_downvote.grouped_awards.keys.sort).to eq %w(thumbsdown thumbsup)
+ expect(note_without_downvote.grouped_awards.keys.sort).to eq %w[thumbsdown thumbsup]
end
it "doesn't include unused thumbs buttons in summary" do
@@ -124,11 +124,11 @@ RSpec.describe Awardable do
it "includes used thumbs buttons when disabled in project" do
note_with_downvote.project.show_default_award_emojis = false
- expect(note_with_downvote.grouped_awards.keys).to eq %w(thumbsdown)
+ expect(note_with_downvote.grouped_awards.keys).to eq %w[thumbsdown]
end
it "includes used thumbs buttons in summary" do
- expect(note_with_downvote.grouped_awards(with_thumbs: false).keys).to eq %w(thumbsdown)
+ expect(note_with_downvote.grouped_awards(with_thumbs: false).keys).to eq %w[thumbsdown]
end
end
end
diff --git a/spec/models/concerns/case_sensitivity_spec.rb b/spec/models/concerns/case_sensitivity_spec.rb
index 6e624c687c4..a8e52904873 100644
--- a/spec/models/concerns/case_sensitivity_spec.rb
+++ b/spec/models/concerns/case_sensitivity_spec.rb
@@ -21,11 +21,11 @@ RSpec.describe CaseSensitivity do
end
it 'finds multiple instances by a single attribute regardless of case' do
- expect(model.iwhere(path: %w(MODEL-1 model-2))).to contain_exactly(model_1, model_2)
+ expect(model.iwhere(path: %w[MODEL-1 model-2])).to contain_exactly(model_1, model_2)
end
it 'finds instances by multiple attributes' do
- expect(model.iwhere(path: %w(MODEL-1 model-2), name: 'model 1'))
+ expect(model.iwhere(path: %w[MODEL-1 model-2], name: 'model 1'))
.to contain_exactly(model_1)
end
@@ -34,7 +34,7 @@ RSpec.describe CaseSensitivity do
end
it 'builds a query using LOWER' do
- query = model.iwhere(path: %w(MODEL-1 model-2), name: 'model 1').to_sql
+ query = model.iwhere(path: %w[MODEL-1 model-2], name: 'model 1').to_sql
expected_query = <<~QRY.strip
SELECT \"namespaces\".* FROM \"namespaces\" WHERE (LOWER(\"namespaces\".\"path\") IN (LOWER('MODEL-1'), LOWER('model-2'))) AND (LOWER(\"namespaces\".\"name\") = LOWER('model 1'))
QRY
diff --git a/spec/models/concerns/ci/has_status_spec.rb b/spec/models/concerns/ci/has_status_spec.rb
index 5e0a430aa13..95f17c4f854 100644
--- a/spec/models/concerns/ci/has_status_spec.rb
+++ b/spec/models/concerns/ci/has_status_spec.rb
@@ -55,6 +55,22 @@ RSpec.describe Ci::HasStatus, feature_category: :continuous_integration do
it { is_expected.to eq 'waiting_for_resource' }
end
+ context 'all waiting for callback' do
+ let!(:statuses) do
+ [create(type, status: :waiting_for_callback), create(type, status: :waiting_for_callback)]
+ end
+
+ it { is_expected.to eq 'waiting_for_callback' }
+ end
+
+ context 'at least one waiting for callback' do
+ let!(:statuses) do
+ [create(type, status: :success), create(type, status: :waiting_for_callback)]
+ end
+
+ it { is_expected.to eq 'waiting_for_callback' }
+ end
+
context 'all preparing' do
let!(:statuses) do
[create(type, status: :preparing), create(type, status: :preparing)]
@@ -225,7 +241,7 @@ RSpec.describe Ci::HasStatus, feature_category: :continuous_integration do
end
end
- %i[created waiting_for_resource preparing running pending success
+ %i[created waiting_for_callback waiting_for_resource preparing running pending success
failed canceled skipped].each do |status|
it_behaves_like 'having a job', status
end
@@ -271,7 +287,7 @@ RSpec.describe Ci::HasStatus, feature_category: :continuous_integration do
describe '.alive' do
subject { CommitStatus.alive }
- %i[running pending waiting_for_resource preparing created].each do |status|
+ %i[running pending waiting_for_callback waiting_for_resource preparing created].each do |status|
it_behaves_like 'containing the job', status
end
@@ -283,7 +299,7 @@ RSpec.describe Ci::HasStatus, feature_category: :continuous_integration do
describe '.alive_or_scheduled' do
subject { CommitStatus.alive_or_scheduled }
- %i[running pending waiting_for_resource preparing created scheduled].each do |status|
+ %i[running pending waiting_for_callback waiting_for_resource preparing created scheduled].each do |status|
it_behaves_like 'containing the job', status
end
@@ -319,7 +335,7 @@ RSpec.describe Ci::HasStatus, feature_category: :continuous_integration do
describe '.cancelable' do
subject { CommitStatus.cancelable }
- %i[running pending waiting_for_resource preparing created scheduled].each do |status|
+ %i[running pending waiting_for_callback waiting_for_resource preparing created scheduled].each do |status|
it_behaves_like 'containing the job', status
end
diff --git a/spec/models/concerns/ci/partitionable/switch_spec.rb b/spec/models/concerns/ci/partitionable/switch_spec.rb
index 0041a33e50e..c6e2ed265bd 100644
--- a/spec/models/concerns/ci/partitionable/switch_spec.rb
+++ b/spec/models/concerns/ci/partitionable/switch_spec.rb
@@ -31,8 +31,6 @@ RSpec.describe Ci::Partitionable::Switch, :aggregate_failures do
end
before do
- allow(ActiveSupport::DescendantsTracker).to receive(:store_inherited)
-
create_tables(<<~SQL)
CREATE TABLE _test_ci_jobs_metadata(
id serial NOT NULL PRIMARY KEY,
@@ -78,6 +76,15 @@ RSpec.describe Ci::Partitionable::Switch, :aggregate_failures do
)
end
+ # the models defined here are leaked to other tests through
+ # `ActiveRecord::Base.descendants` and we need to counter the side effects
+ # from this. We redefine the method so that we don't check the FF existence
+ # outside of the examples here.
+ # `ActiveSupport::DescendantsTracker.clear` doesn't work with cached classes.
+ after do
+ model.define_singleton_method(:routing_table_enabled?) { false }
+ end
+
it { expect(model).not_to be_routing_class }
it { expect(partitioned_model).to be_routing_class }
diff --git a/spec/models/concerns/enums/sbom_spec.rb b/spec/models/concerns/enums/sbom_spec.rb
index 41670880630..e2f56cc637d 100644
--- a/spec/models/concerns/enums/sbom_spec.rb
+++ b/spec/models/concerns/enums/sbom_spec.rb
@@ -22,7 +22,8 @@ RSpec.describe Enums::Sbom, feature_category: :dependency_management do
:apk | 9
:rpm | 10
:deb | 11
- :cbl_mariner | 12
+ 'cbl-mariner' | 12
+ :wolfi | 13
'unknown-pkg-manager' | 0
'Python (unknown)' | 0
end
diff --git a/spec/models/concerns/featurable_spec.rb b/spec/models/concerns/featurable_spec.rb
index bf104fe1b30..97ad4fc8bdd 100644
--- a/spec/models/concerns/featurable_spec.rb
+++ b/spec/models/concerns/featurable_spec.rb
@@ -9,7 +9,7 @@ RSpec.describe Featurable do
self.table_name = 'project_features'
- set_available_features %i(feature1 feature2 feature3)
+ set_available_features %i[feature1 feature2 feature3]
def feature1_access_level
Featurable::DISABLED
diff --git a/spec/models/concerns/has_user_type_spec.rb b/spec/models/concerns/has_user_type_spec.rb
index 54614ec2b21..effb588e55f 100644
--- a/spec/models/concerns/has_user_type_spec.rb
+++ b/spec/models/concerns/has_user_type_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe User, feature_category: :system_access do
- User::USER_TYPES.keys.each do |type| # rubocop:disable RSpec/UselessDynamicDefinition
+ User::USER_TYPES.each_key do |type| # rubocop:disable RSpec/UselessDynamicDefinition -- `type` used in `let`
let_it_be(type) { create(:user, username: type, user_type: type) }
end
let(:bots) { User::BOT_USER_TYPES.map { |type| public_send(type) } }
diff --git a/spec/models/concerns/ignorable_columns_spec.rb b/spec/models/concerns/ignorable_columns_spec.rb
index c97dc606159..339f06f9c45 100644
--- a/spec/models/concerns/ignorable_columns_spec.rb
+++ b/spec/models/concerns/ignorable_columns_spec.rb
@@ -14,13 +14,13 @@ RSpec.describe IgnorableColumns do
it 'adds columns to ignored_columns' do
expect do
subject.ignore_columns(:name, :created_at, remove_after: '2019-12-01', remove_with: '12.6')
- end.to change { subject.ignored_columns }.from([]).to(%w(name created_at))
+ end.to change { subject.ignored_columns }.from([]).to(%w[name created_at])
end
it 'adds columns to ignored_columns (array version)' do
expect do
subject.ignore_columns(%i[name created_at], remove_after: '2019-12-01', remove_with: '12.6')
- end.to change { subject.ignored_columns }.from([]).to(%w(name created_at))
+ end.to change { subject.ignored_columns }.from([]).to(%w[name created_at])
end
it 'requires remove_after attribute to be set' do
diff --git a/spec/models/concerns/issuable_spec.rb b/spec/models/concerns/issuable_spec.rb
index 705f8f46a90..d61a465b39f 100644
--- a/spec/models/concerns/issuable_spec.rb
+++ b/spec/models/concerns/issuable_spec.rb
@@ -597,7 +597,7 @@ RSpec.describe Issuable, feature_category: :team_planning do
expect(builder).to receive(:build).with(
user: user,
changes: hash_including(
- 'severity' => %w(unknown low)
+ 'severity' => %w[unknown low]
))
issue.to_hook_data(user, old_associations: { severity: 'unknown' })
@@ -618,7 +618,7 @@ RSpec.describe Issuable, feature_category: :team_planning do
expect(builder).to receive(:build).with(
user: user,
changes: hash_including(
- 'escalation_status' => %i(triggered acknowledged)
+ 'escalation_status' => %i[triggered acknowledged]
))
issue.to_hook_data(user, old_associations: { escalation_status: :triggered })
diff --git a/spec/models/concerns/pg_full_text_searchable_spec.rb b/spec/models/concerns/pg_full_text_searchable_spec.rb
index 059df64f7d0..8e3b65cf125 100644
--- a/spec/models/concerns/pg_full_text_searchable_spec.rb
+++ b/spec/models/concerns/pg_full_text_searchable_spec.rb
@@ -18,7 +18,7 @@ RSpec.describe PgFullTextSearchable, feature_category: :global_search do
before_validation -> { self.work_item_type_id = ::WorkItems::Type.default_issue_type.id }
def persist_pg_full_text_search_vector(search_vector)
- Issues::SearchData.upsert({ project_id: project_id, issue_id: id, search_vector: search_vector }, unique_by: %i(project_id issue_id))
+ Issues::SearchData.upsert({ project_id: project_id, issue_id: id, search_vector: search_vector }, unique_by: %i[project_id issue_id])
end
def self.name
@@ -95,7 +95,7 @@ RSpec.describe PgFullTextSearchable, feature_category: :global_search do
matching_object = model_class.create!(project: project, namespace: project.project_namespace, title: 'english', description: 'some description')
matching_object.update_search_data!
- expect(model_class.pg_full_text_search('english', matched_columns: %w(title))).to contain_exactly(matching_object)
+ expect(model_class.pg_full_text_search('english', matched_columns: %w[title])).to contain_exactly(matching_object)
end
it 'uses prefix matching' do
diff --git a/spec/models/concerns/project_features_compatibility_spec.rb b/spec/models/concerns/project_features_compatibility_spec.rb
index f168bedc8eb..46390fa735b 100644
--- a/spec/models/concerns/project_features_compatibility_spec.rb
+++ b/spec/models/concerns/project_features_compatibility_spec.rb
@@ -4,12 +4,12 @@ require 'spec_helper'
RSpec.describe ProjectFeaturesCompatibility do
let(:project) { create(:project) }
- let(:features_enabled) { %w(issues wiki builds merge_requests snippets security_and_compliance) }
+ let(:features_enabled) { %w[issues wiki builds merge_requests snippets security_and_compliance] }
let(:features) do
- features_enabled + %w(
+ features_enabled + %w[
repository pages operations container_registry package_registry environments feature_flags releases
monitor infrastructure
- )
+ ]
end
# We had issues_enabled, snippets_enabled, builds_enabled, merge_requests_enabled and issues_enabled fields on projects table
diff --git a/spec/models/concerns/reactive_caching_spec.rb b/spec/models/concerns/reactive_caching_spec.rb
index 039b9e574fe..324759fc7ee 100644
--- a/spec/models/concerns/reactive_caching_spec.rb
+++ b/spec/models/concerns/reactive_caching_spec.rb
@@ -176,7 +176,7 @@ RSpec.describe ReactiveCaching, :use_clean_rails_memory_store_caching do
describe '.reactive_cache_worker_finder' do
context 'with default reactive_cache_worker_finder' do
- let(:args) { %w(other args) }
+ let(:args) { %w[other args] }
before do
allow(instance.class).to receive(:find_by).with(id: instance.id)
@@ -192,7 +192,7 @@ RSpec.describe ReactiveCaching, :use_clean_rails_memory_store_caching do
end
context 'with custom reactive_cache_worker_finder' do
- let(:args) { %w(arg1 arg2) }
+ let(:args) { %w[arg1 arg2] }
let(:instance) { custom_finder_cache_test.new(666, &calculation) }
let(:custom_finder_cache_test) do
diff --git a/spec/models/concerns/reset_on_column_errors_spec.rb b/spec/models/concerns/reset_on_column_errors_spec.rb
index 38ba0f447f5..96bee128f7e 100644
--- a/spec/models/concerns/reset_on_column_errors_spec.rb
+++ b/spec/models/concerns/reset_on_column_errors_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe ResetOnColumnErrors, :delete, feature_category: :shared do
+RSpec.describe ResetOnColumnErrors, :delete, feature_category: :shared, query_analyzers: false do
let(:test_reviewer_model) do
Class.new(ApplicationRecord) do
self.table_name = '_test_reviewers_table'
diff --git a/spec/models/concerns/sortable_spec.rb b/spec/models/concerns/sortable_spec.rb
index f1ae89f33af..98f4ab4f521 100644
--- a/spec/models/concerns/sortable_spec.rb
+++ b/spec/models/concerns/sortable_spec.rb
@@ -14,14 +14,14 @@ RSpec.describe Sortable do
it 'allows secondary ordering by id ascending' do
orders = arel_orders(sorted_relation.with_order_id_asc)
- expect(orders.map { |arel| arel.expr.name }).to eq(%w(created_at id))
+ expect(orders.map { |arel| arel.expr.name }).to eq(%w[created_at id])
expect(orders).to all(be_kind_of(Arel::Nodes::Ascending))
end
it 'allows secondary ordering by id descending' do
orders = arel_orders(sorted_relation.with_order_id_desc)
- expect(orders.map { |arel| arel.expr.name }).to eq(%w(created_at id))
+ expect(orders.map { |arel| arel.expr.name }).to eq(%w[created_at id])
expect(orders.first).to be_kind_of(Arel::Nodes::Ascending)
expect(orders.last).to be_kind_of(Arel::Nodes::Descending)
end
@@ -123,24 +123,24 @@ RSpec.describe Sortable do
let!(:group4) { create(:group, name: 'bbb', id: 4, created_at: ref_time, updated_at: ref_time - 15.seconds) }
it 'sorts groups by id' do
- expect(ordered_group_names('id_asc')).to eq(%w(aa AAA BB bbb))
- expect(ordered_group_names('id_desc')).to eq(%w(bbb BB AAA aa))
+ expect(ordered_group_names('id_asc')).to eq(%w[aa AAA BB bbb])
+ expect(ordered_group_names('id_desc')).to eq(%w[bbb BB AAA aa])
end
it 'sorts groups by name via case-insensitive comparision' do
- expect(ordered_group_names('name_asc')).to eq(%w(aa AAA BB bbb))
- expect(ordered_group_names('name_desc')).to eq(%w(bbb BB AAA aa))
+ expect(ordered_group_names('name_asc')).to eq(%w[aa AAA BB bbb])
+ expect(ordered_group_names('name_desc')).to eq(%w[bbb BB AAA aa])
end
it 'sorts groups by created_at' do
- expect(ordered_group_names('created_asc')).to eq(%w(aa AAA BB bbb))
- expect(ordered_group_names('created_desc')).to eq(%w(bbb BB AAA aa))
- expect(ordered_group_names('created_date')).to eq(%w(bbb BB AAA aa))
+ expect(ordered_group_names('created_asc')).to eq(%w[aa AAA BB bbb])
+ expect(ordered_group_names('created_desc')).to eq(%w[bbb BB AAA aa])
+ expect(ordered_group_names('created_date')).to eq(%w[bbb BB AAA aa])
end
it 'sorts groups by updated_at' do
- expect(ordered_group_names('updated_asc')).to eq(%w(bbb BB AAA aa))
- expect(ordered_group_names('updated_desc')).to eq(%w(aa AAA BB bbb))
+ expect(ordered_group_names('updated_asc')).to eq(%w[bbb BB AAA aa])
+ expect(ordered_group_names('updated_desc')).to eq(%w[aa AAA BB bbb])
end
end
end
diff --git a/spec/models/concerns/token_authenticatable_spec.rb b/spec/models/concerns/token_authenticatable_spec.rb
index 822e2817d84..9bd20676c0e 100644
--- a/spec/models/concerns/token_authenticatable_spec.rb
+++ b/spec/models/concerns/token_authenticatable_spec.rb
@@ -260,7 +260,7 @@ RSpec.describe Ci::Build, 'TokenAuthenticatable' do
it 'persists a new token' do
build.save!
- build.token.yield_self do |previous_token|
+ build.token.then do |previous_token|
build.reset_token!
expect(build.token).not_to eq previous_token
diff --git a/spec/models/concerns/use_sql_function_for_primary_key_lookups_spec.rb b/spec/models/concerns/use_sql_function_for_primary_key_lookups_spec.rb
new file mode 100644
index 00000000000..f6f53c9aad5
--- /dev/null
+++ b/spec/models/concerns/use_sql_function_for_primary_key_lookups_spec.rb
@@ -0,0 +1,181 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe UseSqlFunctionForPrimaryKeyLookups, feature_category: :groups_and_projects do
+ let_it_be(:project) { create(:project) }
+ let_it_be(:another_project) { create(:project) }
+
+ let(:model) do
+ Class.new(ApplicationRecord) do
+ self.table_name = :projects
+
+ include UseSqlFunctionForPrimaryKeyLookups
+ end
+ end
+
+ context 'when the use_sql_functions_for_primary_key_lookups FF is on' do
+ before do
+ stub_feature_flags(use_sql_functions_for_primary_key_lookups: true)
+ end
+
+ it 'loads the correct record' do
+ expect(model.find(project.id).id).to eq(project.id)
+ end
+
+ it 'uses the fuction-based finder query' do
+ query = <<~SQL
+ SELECT "projects".* FROM find_projects_by_id(#{project.id})#{' '}
+ "projects" WHERE ("projects"."id" IS NOT NULL) LIMIT 1
+ SQL
+ query_log = ActiveRecord::QueryRecorder.new { model.find(project.id) }.log
+
+ expect(query_log).to match_array(include(query.tr("\n", '')))
+ end
+
+ it 'uses query cache', :use_sql_query_cache do
+ query = <<~SQL
+ SELECT "projects".* FROM find_projects_by_id(#{project.id})#{' '}
+ "projects" WHERE ("projects"."id" IS NOT NULL) LIMIT 1
+ SQL
+
+ recorder = ActiveRecord::QueryRecorder.new do
+ model.find(project.id)
+ model.find(project.id)
+ model.find(project.id)
+ end
+
+ expect(recorder.data.each_value.first[:count]).to eq(1)
+ expect(recorder.cached).to include(query.tr("\n", ''))
+ end
+
+ context 'when the model has ignored columns' do
+ around do |example|
+ model.ignored_columns = %i[path]
+ example.run
+ model.ignored_columns = []
+ end
+
+ it 'enumerates the column names' do
+ column_list = model.columns.map do |column|
+ %("projects"."#{column.name}")
+ end.join(', ')
+
+ expect(column_list).not_to include(%("projects"."path"))
+
+ query = <<~SQL
+ SELECT #{column_list} FROM find_projects_by_id(#{project.id})#{' '}
+ "projects" WHERE ("projects"."id" IS NOT NULL) LIMIT 1
+ SQL
+ query_log = ActiveRecord::QueryRecorder.new { model.find(project.id) }.log
+
+ expect(query_log).to match_array(include(query.tr("\n", '')))
+ end
+ end
+
+ context 'when there are scope attributes' do
+ let(:scoped_model) do
+ Class.new(model) do
+ default_scope { where.not(path: nil) } # rubocop: disable Cop/DefaultScope -- Needed for testing a specific case
+ end
+ end
+
+ it 'loads the correct record' do
+ expect(scoped_model.find(project.id).id).to eq(project.id)
+ end
+
+ it 'does not use the function-based finder query' do
+ query_log = ActiveRecord::QueryRecorder.new { scoped_model.find(project.id) }.log
+
+ expect(query_log).not_to include(match(/find_projects_by_id/))
+ end
+ end
+
+ context 'when there are multiple arguments' do
+ it 'loads the correct records' do
+ expect(model.find(project.id, another_project.id).map(&:id)).to match_array([project.id, another_project.id])
+ end
+
+ it 'does not use the function-based finder query' do
+ query_log = ActiveRecord::QueryRecorder.new { model.find(project.id, another_project.id) }.log
+
+ expect(query_log).not_to include(match(/find_projects_by_id/))
+ end
+ end
+
+ context 'when there is block given' do
+ it 'loads the correct records' do
+ expect(model.find(0) { |p| p.path == project.path }.id).to eq(project.id)
+ end
+
+ it 'does not use the function-based finder query' do
+ query_log = ActiveRecord::QueryRecorder.new { model.find(0) { |p| p.path == project.path } }.log
+
+ expect(query_log).not_to include(match(/find_projects_by_id/))
+ end
+ end
+
+ context 'when there is no primary key defined' do
+ let(:model_without_pk) do
+ Class.new(model) do
+ def self.primary_key
+ nil
+ end
+ end
+ end
+
+ it 'raises ActiveRecord::UnknownPrimaryKey' do
+ expect { model_without_pk.find(0) }.to raise_error ActiveRecord::UnknownPrimaryKey
+ end
+ end
+
+ context 'when id is provided as an array' do
+ it 'returns the correct record as an array' do
+ expect(model.find([project.id]).map(&:id)).to eq([project.id])
+ end
+
+ it 'does use the function-based finder query' do
+ query_log = ActiveRecord::QueryRecorder.new { model.find([project.id]) }.log
+
+ expect(query_log).to include(match(/find_projects_by_id/))
+ end
+
+ context 'when array has multiple elements' do
+ it 'does not use the function-based finder query' do
+ query_log = ActiveRecord::QueryRecorder.new { model.find([project.id, another_project.id]) }.log
+
+ expect(query_log).not_to include(match(/find_projects_by_id/))
+ end
+ end
+ end
+
+ context 'when the provided id is null' do
+ it 'raises ActiveRecord::RecordNotFound' do
+ expect { model.find(nil) }.to raise_error ActiveRecord::RecordNotFound, "Couldn't find without an ID"
+ end
+ end
+
+ context 'when the provided id is not a string that can cast to numeric' do
+ it 'raises ActiveRecord::RecordNotFound' do
+ expect { model.find('foo') }.to raise_error ActiveRecord::RecordNotFound, "Couldn't find with 'id'=foo"
+ end
+ end
+ end
+
+ context 'when the use_sql_functions_for_primary_key_lookups FF is off' do
+ before do
+ stub_feature_flags(use_sql_functions_for_primary_key_lookups: false)
+ end
+
+ it 'loads the correct record' do
+ expect(model.find(project.id).id).to eq(project.id)
+ end
+
+ it 'uses the SQL-based finder query' do
+ expected_query = %(SELECT "projects".* FROM \"projects\" WHERE "projects"."id" = #{project.id} LIMIT 1)
+ query_log = ActiveRecord::QueryRecorder.new { model.find(project.id) }.log
+
+ expect(query_log).to match_array(include(expected_query))
+ end
+ end
+end
diff --git a/spec/models/container_repository_spec.rb b/spec/models/container_repository_spec.rb
index 93fe070e5c4..027fd20462b 100644
--- a/spec/models/container_repository_spec.rb
+++ b/spec/models/container_repository_spec.rb
@@ -675,6 +675,101 @@ RSpec.describe ContainerRepository, :aggregate_failures, feature_category: :cont
end
end
+ describe '#tags_page' do
+ let_it_be(:page_size) { 100 }
+ let_it_be(:before) { 'before' }
+ let_it_be(:last) { 'last' }
+ let_it_be(:sort) { '-name' }
+ let_it_be(:name) { 'repo' }
+
+ subject do
+ repository.tags_page(before: before, last: last, sort: sort, name: name, page_size: page_size)
+ end
+
+ before do
+ allow(repository).to receive(:migrated?).and_return(true)
+ end
+
+ it 'calls GitlabApiClient#tags and passes parameters' do
+ allow(repository.gitlab_api_client).to receive(:tags).and_return({})
+ expect(repository.gitlab_api_client).to receive(:tags).with(
+ repository.path, page_size: page_size, before: before, last: last, sort: sort, name: name)
+
+ subject
+ end
+
+ context 'with a call to tags' do
+ let_it_be(:tags_response) do
+ [
+ {
+ name: '0.1.0',
+ digest: 'sha256:6c3c624b58dbbcd3c0dd82b4c53f04194d1247c6eebdaab7c610cf7d6670',
+ config_digest: 'sha256:66b1132a0173910b01ee69583bbf2f7f1e4462c99efbe1b9ab5bf',
+ media_type: 'application/vnd.oci.image.manifest.v1+json',
+ size_bytes: 1234567890,
+ created_at: 5.minutes.ago,
+ updated_at: 5.minutes.ago
+ },
+ {
+ name: 'latest',
+ digest: 'sha256:6c3c624b58dbbcd3c0dd82b4c53f04191247c6eebdaab7c610cf7d66709b3',
+ config_digest: 'sha256:66b1132a0173910b01ee694462c99efbe1b9ab5bf8083231232312',
+ media_type: 'application/vnd.oci.image.manifest.v1+json',
+ size_bytes: 1234567892,
+ created_at: 10.minutes.ago,
+ updated_at: 10.minutes.ago
+ }
+ ]
+ end
+
+ let_it_be(:response_body) do
+ {
+ pagination: {
+ previous: { uri: URI('/test?before=prev-cursor') },
+ next: { uri: URI('/test?last=next-cursor') }
+ },
+ response_body: ::Gitlab::Json.parse(tags_response.to_json)
+ }
+ end
+
+ before do
+ allow(repository.gitlab_api_client).to receive(:tags).and_return(response_body)
+ end
+
+ it 'returns tags and parses the previous and next cursors' do
+ return_value = subject
+
+ expect(return_value[:pagination]).to eq(response_body[:pagination])
+
+ return_value[:tags].each_with_index do |tag, index|
+ expected_revision = tags_response[index][:config_digest].to_s.split(':')[1]
+
+ expect(tag.is_a?(ContainerRegistry::Tag)).to eq(true)
+ expect(tag).to have_attributes(
+ repository: repository,
+ name: tags_response[index][:name],
+ digest: tags_response[index][:digest],
+ total_size: tags_response[index][:size_bytes],
+ revision: expected_revision,
+ short_revision: expected_revision[0..8],
+ created_at: DateTime.rfc3339(tags_response[index][:created_at].rfc3339),
+ updated_at: DateTime.rfc3339(tags_response[index][:updated_at].rfc3339)
+ )
+ end
+ end
+ end
+
+ context 'calling on a non migrated repository' do
+ before do
+ allow(repository).to receive(:migrated?).and_return(false)
+ end
+
+ it 'raises an Argument error' do
+ expect { repository.tags_page }.to raise_error(ArgumentError, 'not a migrated repository')
+ end
+ end
+ end
+
describe '#tags_count' do
it 'returns the count of tags' do
expect(repository.tags_count).to eq(1)
@@ -720,7 +815,7 @@ RSpec.describe ContainerRepository, :aggregate_failures, feature_category: :cont
end
end
- describe '#delete_tag_by_name' do
+ describe '#delete_tag' do
let(:repository) do
create(
:container_repository,
@@ -733,22 +828,22 @@ RSpec.describe ContainerRepository, :aggregate_failures, feature_category: :cont
context 'when action succeeds' do
it 'returns status that indicates success' do
expect(repository.client)
- .to receive(:delete_repository_tag_by_name)
+ .to receive(:delete_repository_tag_by_digest)
.with(repository.path, "latest")
.and_return(true)
- expect(repository.delete_tag_by_name('latest')).to be_truthy
+ expect(repository.delete_tag('latest')).to be_truthy
end
end
context 'when action fails' do
it 'returns status that indicates failure' do
expect(repository.client)
- .to receive(:delete_repository_tag_by_name)
+ .to receive(:delete_repository_tag_by_digest)
.with(repository.path, "latest")
.and_return(false)
- expect(repository.delete_tag_by_name('latest')).to be_falsey
+ expect(repository.delete_tag('latest')).to be_falsey
end
end
end
diff --git a/spec/models/deployment_spec.rb b/spec/models/deployment_spec.rb
index 639b149e2ae..ee48e8cac6c 100644
--- a/spec/models/deployment_spec.rb
+++ b/spec/models/deployment_spec.rb
@@ -1336,7 +1336,7 @@ RSpec.describe Deployment, feature_category: :continuous_delivery do
let(:job_status) { :created }
it_behaves_like 'gracefully handling error' do
- let(:error_message) { %{Status cannot transition via \"create\"} }
+ let(:error_message) { %(Status cannot transition via \"create\") }
end
end
@@ -1344,7 +1344,7 @@ RSpec.describe Deployment, feature_category: :continuous_delivery do
let(:job_status) { :manual }
it_behaves_like 'gracefully handling error' do
- let(:error_message) { %{Status cannot transition via \"block\"} }
+ let(:error_message) { %(Status cannot transition via \"block\") }
end
end
@@ -1374,7 +1374,7 @@ RSpec.describe Deployment, feature_category: :continuous_delivery do
let(:job_status) { :created }
it_behaves_like 'gracefully handling error' do
- let(:error_message) { %{Status cannot transition via \"create\"} }
+ let(:error_message) { %(Status cannot transition via \"create\") }
end
end
@@ -1382,7 +1382,7 @@ RSpec.describe Deployment, feature_category: :continuous_delivery do
let(:job_status) { :manual }
it_behaves_like 'gracefully handling error' do
- let(:error_message) { %{Status cannot transition via \"block\"} }
+ let(:error_message) { %(Status cannot transition via \"block\") }
end
end
@@ -1390,7 +1390,7 @@ RSpec.describe Deployment, feature_category: :continuous_delivery do
let(:job_status) { :running }
it_behaves_like 'gracefully handling error' do
- let(:error_message) { %{Status cannot transition via \"run\"} }
+ let(:error_message) { %(Status cannot transition via \"run\") }
end
end
diff --git a/spec/models/diff_viewer/base_spec.rb b/spec/models/diff_viewer/base_spec.rb
index 57c62788ee9..8ab7b090928 100644
--- a/spec/models/diff_viewer/base_spec.rb
+++ b/spec/models/diff_viewer/base_spec.rb
@@ -13,7 +13,7 @@ RSpec.describe DiffViewer::Base do
Class.new(described_class) do
include DiffViewer::ServerSide
- self.extensions = %w(jpg)
+ self.extensions = %w[jpg]
self.binary = true
self.collapse_limit = 1.megabyte
self.size_limit = 5.megabytes
@@ -55,7 +55,7 @@ RSpec.describe DiffViewer::Base do
before do
allow(diff_file).to receive(:renamed_file?).and_return(true)
- viewer_class.extensions = %w(notjpg)
+ viewer_class.extensions = %w[notjpg]
end
it 'returns false' do
diff --git a/spec/models/email_spec.rb b/spec/models/email_spec.rb
index b76063bfa1a..ecb8f72470d 100644
--- a/spec/models/email_spec.rb
+++ b/spec/models/email_spec.rb
@@ -89,7 +89,7 @@ RSpec.describe Email do
end
context 'when the confirmation period has expired' do
- let(:confirmation_sent_at) { expired_confirmation_sent_at }
+ let(:confirmation_sent_at) { expired_confirmation_sent_at }
it_behaves_like 'unconfirmed email'
@@ -101,7 +101,7 @@ RSpec.describe Email do
end
context 'when the confirmation period has not expired' do
- let(:confirmation_sent_at) { extant_confirmation_sent_at }
+ let(:confirmation_sent_at) { extant_confirmation_sent_at }
it_behaves_like 'unconfirmed email'
@@ -138,7 +138,7 @@ RSpec.describe Email do
end
context 'when the confirmation period has expired' do
- let(:confirmation_sent_at) { expired_confirmation_sent_at }
+ let(:confirmation_sent_at) { expired_confirmation_sent_at }
it_behaves_like 'unconfirmed email'
it_behaves_like 'confirms the email on force_confirm'
diff --git a/spec/models/environment_spec.rb b/spec/models/environment_spec.rb
index dcfee7fcc8c..33142922670 100644
--- a/spec/models/environment_spec.rb
+++ b/spec/models/environment_spec.rb
@@ -166,6 +166,37 @@ RSpec.describe Environment, :use_clean_rails_memory_store_caching, feature_categ
end
end
+ describe '#long_stopping?' do
+ subject { environment1.long_stopping? }
+
+ let(:long_ago) { (described_class::LONG_STOP + 1.day).ago }
+ let(:not_long_ago) { (described_class::LONG_STOP - 1.day).ago }
+
+ context 'when a stopping environment has not been updated recently' do
+ let!(:environment1) { create(:environment, state: 'stopping', project: project, updated_at: long_ago) }
+
+ it { is_expected.to eq(true) }
+ end
+
+ context 'when a stopping environment has been updated recently' do
+ let!(:environment1) { create(:environment, state: 'stopping', project: project, updated_at: not_long_ago) }
+
+ it { is_expected.to eq(false) }
+ end
+
+ context 'when a non stopping environment has not been updated recently' do
+ let!(:environment1) { create(:environment, project: project, updated_at: long_ago) }
+
+ it { is_expected.to eq(false) }
+ end
+
+ context 'when a non stopping environment has been updated recently' do
+ let!(:environment1) { create(:environment, project: project, updated_at: not_long_ago) }
+
+ it { is_expected.to eq(false) }
+ end
+ end
+
describe ".stopped_review_apps" do
let_it_be(:project) { create(:project, :repository) }
let_it_be(:old_stopped_review_env) { create(:environment, :with_review_app, :stopped, created_at: 31.days.ago, project: project) }
@@ -406,6 +437,47 @@ RSpec.describe Environment, :use_clean_rails_memory_store_caching, feature_categ
end
end
+ describe '.long_stopping' do
+ subject { described_class.long_stopping }
+
+ let_it_be(:project) { create(:project) }
+ let(:environment) { create(:environment, project: project) }
+ let(:long) { (described_class::LONG_STOP + 1.day).ago }
+ let(:short) { (described_class::LONG_STOP - 1.day).ago }
+
+ context 'when a stopping environment has not been updated recently' do
+ before do
+ environment.update!(state: :stopping, updated_at: long)
+ end
+
+ it { is_expected.to eq([environment]) }
+ end
+
+ context 'when a stopping environment has been updated recently' do
+ before do
+ environment.update!(state: :stopping, updated_at: short)
+ end
+
+ it { is_expected.to be_empty }
+ end
+
+ context 'when a non stopping environment has not been updated recently' do
+ before do
+ environment.update!(state: :available, updated_at: long)
+ end
+
+ it { is_expected.to be_empty }
+ end
+
+ context 'when a non stopping environment has been updated recently' do
+ before do
+ environment.update!(state: :available, updated_at: short)
+ end
+
+ it { is_expected.to be_empty }
+ end
+ end
+
describe '.pluck_names' do
subject { described_class.pluck_names }
@@ -1361,7 +1433,7 @@ RSpec.describe Environment, :use_clean_rails_memory_store_caching, feature_categ
end
context 'reactive cache has pod data' do
- let(:cache_data) { Hash(pods: %w(pod1 pod2)) }
+ let(:cache_data) { Hash(pods: %w[pod1 pod2]) }
before do
stub_reactive_cache(environment, cache_data)
@@ -1390,9 +1462,9 @@ RSpec.describe Environment, :use_clean_rails_memory_store_caching, feature_categ
it 'returns cache data from the deployment platform' do
expect(environment.deployment_platform).to receive(:calculate_reactive_cache_for)
- .with(environment).and_return(pods: %w(pod1 pod2))
+ .with(environment).and_return(pods: %w[pod1 pod2])
- is_expected.to eq(pods: %w(pod1 pod2))
+ is_expected.to eq(pods: %w[pod1 pod2])
end
context 'environment does not have terminals available' do
@@ -1863,8 +1935,8 @@ RSpec.describe Environment, :use_clean_rails_memory_store_caching, feature_categ
end
context 'cached rollout status is present' do
- let(:pods) { %w(pod1 pod2) }
- let(:deployments) { %w(deployment1 deployment2) }
+ let(:pods) { %w[pod1 pod2] }
+ let(:deployments) { %w[deployment1 deployment2] }
before do
stub_reactive_cache(environment, pods: pods, deployments: deployments)
diff --git a/spec/models/group_label_spec.rb b/spec/models/group_label_spec.rb
index 701348baf48..40019fdc94c 100644
--- a/spec/models/group_label_spec.rb
+++ b/spec/models/group_label_spec.rb
@@ -34,7 +34,7 @@ RSpec.describe GroupLabel do
end
it 'uses id when name contains double quote' do
- label = create(:label, name: %q{"irony"})
+ label = create(:label, name: %q("irony"))
expect(label.to_reference(format: :name)).to eq "~#{label.id}"
end
end
diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb
index 96ef36a5b75..2bca73545d0 100644
--- a/spec/models/group_spec.rb
+++ b/spec/models/group_spec.rb
@@ -1659,6 +1659,12 @@ RSpec.describe Group, feature_category: :groups_and_projects do
end
end
+ it 'returns true for a user in parent group' do
+ subgroup = create(:group, parent: group)
+
+ expect(subgroup.member?(user)).to be_truthy
+ end
+
context 'in shared group' do
let(:shared_group) { create(:group) }
let(:member_shared) { create(:user) }
@@ -2053,29 +2059,6 @@ RSpec.describe Group, feature_category: :groups_and_projects do
end
end
- describe '#project_users_with_descendants' do
- let(:user_a) { create(:user) }
- let(:user_b) { create(:user) }
- let(:user_c) { create(:user) }
-
- let(:group) { create(:group) }
- let(:nested_group) { create(:group, parent: group) }
- let(:deep_nested_group) { create(:group, parent: nested_group) }
- let(:project_a) { create(:project, namespace: group) }
- let(:project_b) { create(:project, namespace: nested_group) }
- let(:project_c) { create(:project, namespace: deep_nested_group) }
-
- it 'returns members of all projects in group and subgroups' do
- project_a.add_developer(user_a)
- project_b.add_developer(user_b)
- project_c.add_developer(user_c)
-
- expect(group.project_users_with_descendants).to contain_exactly(user_a, user_b, user_c)
- expect(nested_group.project_users_with_descendants).to contain_exactly(user_b, user_c)
- expect(deep_nested_group.project_users_with_descendants).to contain_exactly(user_c)
- end
- end
-
describe '#refresh_members_authorized_projects' do
let_it_be(:group) { create(:group, :nested) }
let_it_be(:parent_group_user) { create(:user) }
@@ -2953,18 +2936,21 @@ RSpec.describe Group, feature_category: :groups_and_projects do
end
describe '.ids_with_disabled_email' do
- let_it_be(:parent_1) { create(:group, emails_disabled: true) }
+ let_it_be(:parent_1) { create(:group) }
let_it_be(:child_1) { create(:group, parent: parent_1) }
- let_it_be(:parent_2) { create(:group, emails_disabled: false) }
+ let_it_be(:parent_2) { create(:group) }
let_it_be(:child_2) { create(:group, parent: parent_2) }
- let_it_be(:other_group) { create(:group, emails_disabled: false) }
+ let_it_be(:other_group) { create(:group) }
shared_examples 'returns namespaces with disabled email' do
subject(:group_ids_where_email_is_disabled) { described_class.ids_with_disabled_email([child_1, child_2, other_group]) }
- it { is_expected.to eq(Set.new([child_1.id])) }
+ it do
+ parent_1.update_attribute(:emails_enabled, false)
+ is_expected.to eq(Set.new([child_1.id]))
+ end
end
it_behaves_like 'returns namespaces with disabled email'
diff --git a/spec/models/instance_configuration_spec.rb b/spec/models/instance_configuration_spec.rb
index 346f743e8ef..6fbb9245885 100644
--- a/spec/models/instance_configuration_spec.rb
+++ b/spec/models/instance_configuration_spec.rb
@@ -36,7 +36,7 @@ RSpec.describe InstanceConfiguration do
result = subject.settings[:ssh_algorithms_hashes]
- expect(result.map { |a| a[:name] }).to match_array(%w(DSA ECDSA ED25519 RSA))
+ expect(result.map { |a| a[:name] }).to match_array(%w[DSA ECDSA ED25519 RSA])
end
it 'does not include disabled algorithm' do
@@ -45,7 +45,7 @@ RSpec.describe InstanceConfiguration do
result = subject.settings[:ssh_algorithms_hashes]
- expect(result.map { |a| a[:name] }).to match_array(%w(ECDSA ED25519 RSA))
+ expect(result.map { |a| a[:name] }).to match_array(%w[ECDSA ED25519 RSA])
end
def pub_file(exist: true)
diff --git a/spec/models/integration_spec.rb b/spec/models/integration_spec.rb
index d7b69546de6..5af6a592c66 100644
--- a/spec/models/integration_spec.rb
+++ b/spec/models/integration_spec.rb
@@ -157,6 +157,18 @@ RSpec.describe Integration, feature_category: :integrations do
include_examples 'hook scope', 'incident'
end
+ describe '.title' do
+ it 'raises an error' do
+ expect { described_class.title }.to raise_error(NotImplementedError)
+ end
+ end
+
+ describe '.description' do
+ it 'raises an error' do
+ expect { described_class.description }.to raise_error(NotImplementedError)
+ end
+ end
+
describe '#operating?' do
it 'is false when the integration is not active' do
expect(build(:integration).operating?).to eq(false)
@@ -976,7 +988,7 @@ RSpec.describe Integration, feature_category: :integrations do
subject { described_class.available_integration_names }
before do
- allow(described_class).to receive(:integration_names).and_return(%w(foo))
+ allow(described_class).to receive(:integration_names).and_return(%w[foo])
allow(described_class).to receive(:project_specific_integration_names).and_return(['bar'])
allow(described_class).to receive(:dev_integration_names).and_return(['baz'])
end
@@ -1315,6 +1327,7 @@ RSpec.describe Integration, feature_category: :integrations do
describe '#async_execute' do
let(:integration) { described_class.new(id: 123) }
let(:data) { { object_kind: 'build' } }
+ let(:serialized_data) { data.deep_stringify_keys }
let(:supported_events) { %w[push build] }
subject(:async_execute) { integration.async_execute(data) }
@@ -1324,7 +1337,7 @@ RSpec.describe Integration, feature_category: :integrations do
end
it 'queues a Integrations::ExecuteWorker' do
- expect(Integrations::ExecuteWorker).to receive(:perform_async).with(integration.id, data)
+ expect(Integrations::ExecuteWorker).to receive(:perform_async).with(integration.id, serialized_data)
async_execute
end
diff --git a/spec/models/integrations/base_chat_notification_spec.rb b/spec/models/integrations/base_chat_notification_spec.rb
index 497f2f1e7c9..9ad37f40fbc 100644
--- a/spec/models/integrations/base_chat_notification_spec.rb
+++ b/spec/models/integrations/base_chat_notification_spec.rb
@@ -347,12 +347,6 @@ RSpec.describe Integrations::BaseChatNotification, feature_category: :integratio
end
end
- describe '#help' do
- it 'raises an error' do
- expect { subject.help }.to raise_error(NotImplementedError)
- end
- end
-
describe '#event_channel_name' do
it 'returns the channel field name for the given event' do
expect(subject.event_channel_name(:event)).to eq('event_channel')
diff --git a/spec/models/integrations/buildkite_spec.rb b/spec/models/integrations/buildkite_spec.rb
index f3231d50eae..ce31c0b20a3 100644
--- a/spec/models/integrations/buildkite_spec.rb
+++ b/spec/models/integrations/buildkite_spec.rb
@@ -50,7 +50,7 @@ RSpec.describe Integrations::Buildkite, :use_clean_rails_memory_store_caching, f
describe '.supported_events' do
it 'supports push, merge_request, and tag_push events' do
- expect(integration.supported_events).to eq %w(push merge_request tag_push)
+ expect(integration.supported_events).to eq %w[push merge_request tag_push]
end
end
diff --git a/spec/models/integrations/campfire_spec.rb b/spec/models/integrations/campfire_spec.rb
index 38d3d89cdbf..19f819252f8 100644
--- a/spec/models/integrations/campfire_spec.rb
+++ b/spec/models/integrations/campfire_spec.rb
@@ -48,7 +48,7 @@ RSpec.describe Integrations::Campfire, feature_category: :integrations do
)
@sample_data = Gitlab::DataBuilder::Push.build_sample(project, user)
@rooms_url = 'https://project-name.campfirenow.com/rooms.json'
- @auth = %w(verySecret X)
+ @auth = %w[verySecret X]
@headers = { 'Content-Type' => 'application/json; charset=utf-8' }
end
diff --git a/spec/models/integrations/integration_list_spec.rb b/spec/models/integrations/integration_list_spec.rb
index b7ccbcecf6b..4bb7b100bc0 100644
--- a/spec/models/integrations/integration_list_spec.rb
+++ b/spec/models/integrations/integration_list_spec.rb
@@ -12,10 +12,10 @@ RSpec.describe Integrations::IntegrationList, feature_category: :integrations do
describe '#to_array' do
it 'returns array of Integration, columns, and values' do
- expect(subject.to_array).to eq([
+ expect(subject.to_array).to match_array([
Integration,
%w[active category project_id],
- [['true', 'common', projects.first.id], ['true', 'common', projects.second.id]]
+ contain_exactly(['true', 'common', projects.first.id], ['true', 'common', projects.second.id])
])
end
end
diff --git a/spec/models/integrations/jira_spec.rb b/spec/models/integrations/jira_spec.rb
index c87128db221..af021c51035 100644
--- a/spec/models/integrations/jira_spec.rb
+++ b/spec/models/integrations/jira_spec.rb
@@ -603,6 +603,17 @@ RSpec.describe Integrations::Jira, feature_category: :integrations do
jira_integration.client.get('/foo')
end
+ context 'when a custom read_timeout option is passed as an argument' do
+ it 'uses the default GitLab::HTTP timeouts plus a custom read_timeout' do
+ expected_timeouts = Gitlab::HTTP::DEFAULT_TIMEOUT_OPTIONS.merge(read_timeout: 2.minutes, timeout: 2.minutes)
+
+ expect(Gitlab::HTTP_V2::Client).to receive(:httparty_perform_request)
+ .with(Net::HTTP::Get, '/foo', hash_including(expected_timeouts)).and_call_original
+
+ jira_integration.client(read_timeout: 2.minutes).get('/foo')
+ end
+ end
+
context 'with basic auth' do
before do
jira_integration.jira_auth_type = 0
@@ -719,10 +730,10 @@ RSpec.describe Integrations::Jira, feature_category: :integrations do
allow_any_instance_of(JIRA::Resource::Issue).to receive(:key).and_return(issue_key)
allow(JIRA::Resource::Remotelink).to receive(:all).and_return([])
- WebMock.stub_request(:get, issue_url).with(basic_auth: %w(jira-username jira-password))
- WebMock.stub_request(:post, transitions_url).with(basic_auth: %w(jira-username jira-password))
- WebMock.stub_request(:post, comment_url).with(basic_auth: %w(jira-username jira-password))
- WebMock.stub_request(:post, remote_link_url).with(basic_auth: %w(jira-username jira-password))
+ WebMock.stub_request(:get, issue_url).with(basic_auth: %w[jira-username jira-password])
+ WebMock.stub_request(:post, transitions_url).with(basic_auth: %w[jira-username jira-password])
+ WebMock.stub_request(:post, comment_url).with(basic_auth: %w[jira-username jira-password])
+ WebMock.stub_request(:post, remote_link_url).with(basic_auth: %w[jira-username jira-password])
end
let(:external_issue) { ExternalIssue.new('JIRA-123', project) }
@@ -864,7 +875,7 @@ RSpec.describe Integrations::Jira, feature_category: :integrations do
it 'logs exception when transition id is not valid' do
allow(jira_integration).to receive(:log_exception)
- WebMock.stub_request(:post, transitions_url).with(basic_auth: %w(jira-username jira-password)).and_raise("Bad Request")
+ WebMock.stub_request(:post, transitions_url).with(basic_auth: %w[jira-username jira-password]).and_raise("Bad Request")
close_issue
@@ -973,7 +984,7 @@ RSpec.describe Integrations::Jira, feature_category: :integrations do
context 'when a transition fails' do
before do
- WebMock.stub_request(:post, transitions_url).with(basic_auth: %w(jira-username jira-password)).to_return do |request|
+ WebMock.stub_request(:post, transitions_url).with(basic_auth: %w[jira-username jira-password]).to_return do |request|
{ status: request.body.include?('"id":"2"') ? 500 : 200 }
end
end
diff --git a/spec/models/integrations/teamcity_spec.rb b/spec/models/integrations/teamcity_spec.rb
index c4c7202fae0..1537b10ba03 100644
--- a/spec/models/integrations/teamcity_spec.rb
+++ b/spec/models/integrations/teamcity_spec.rb
@@ -308,7 +308,7 @@ RSpec.describe Integrations::Teamcity, :use_clean_rails_memory_store_caching do
def stub_post_to_build_queue(branch:)
teamcity_full_url = "#{teamcity_url}/httpAuth/app/rest/buildQueue"
body ||= %(<build branchName=\"#{branch}\"><buildType id=\"foo\"/></build>)
- auth = %w(mic password)
+ auth = %w[mic password]
stub_full_request(teamcity_full_url, method: :post).with(
basic_auth: auth,
@@ -320,7 +320,7 @@ RSpec.describe Integrations::Teamcity, :use_clean_rails_memory_store_caching do
end
def stub_request(status: 200, body: nil, build_status: 'success')
- auth = %w(mic password)
+ auth = %w[mic password]
body ||= %({"build":{"status":"#{build_status}","id":"666"}})
diff --git a/spec/models/issue_spec.rb b/spec/models/issue_spec.rb
index e7a5a53c6a0..6c8603d7b4c 100644
--- a/spec/models/issue_spec.rb
+++ b/spec/models/issue_spec.rb
@@ -373,10 +373,10 @@ RSpec.describe Issue, feature_category: :team_planning do
describe '.simple_sorts' do
it 'includes all keys' do
expect(described_class.simple_sorts.keys).to include(
- *%w(created_asc created_at_asc created_date created_desc created_at_desc
+ *%w[created_asc created_at_asc created_date created_desc created_at_desc
closest_future_date closest_future_date_asc due_date due_date_asc due_date_desc
id_asc id_desc relative_position relative_position_asc updated_desc updated_asc
- updated_at_asc updated_at_desc title_asc title_desc))
+ updated_at_asc updated_at_desc title_asc title_desc])
end
end
@@ -390,7 +390,7 @@ RSpec.describe Issue, feature_category: :team_planning do
end
it 'returns issues with the given issue types' do
- expect(described_class.with_issue_type(%w(issue incident)))
+ expect(described_class.with_issue_type(%w[issue incident]))
.to contain_exactly(issue, incident)
end
@@ -439,7 +439,7 @@ RSpec.describe Issue, feature_category: :team_planning do
end
it 'returns issues without the given issue types' do
- expect(described_class.without_issue_type(%w(issue incident)))
+ expect(described_class.without_issue_type(%w[issue incident]))
.to contain_exactly(task)
end
diff --git a/spec/models/member_spec.rb b/spec/models/member_spec.rb
index fdd8a610fe4..b4941c71d6a 100644
--- a/spec/models/member_spec.rb
+++ b/spec/models/member_spec.rb
@@ -591,6 +591,22 @@ RSpec.describe Member, feature_category: :groups_and_projects do
it { is_expected.not_to include @member_with_minimal_access }
it { is_expected.not_to include awaiting_group_member }
it { is_expected.not_to include awaiting_project_member }
+
+ context 'when minimal_access is true' do
+ subject { described_class.without_invites_and_requests(minimal_access: true) }
+
+ it { is_expected.to include @owner }
+ it { is_expected.to include @maintainer }
+ it { is_expected.not_to include @invited_member }
+ it { is_expected.to include @accepted_invite_member }
+ it { is_expected.not_to include @requested_member }
+ it { is_expected.to include @accepted_request_member }
+ it { is_expected.to include @blocked_maintainer }
+ it { is_expected.to include @blocked_developer }
+ it { is_expected.to include @member_with_minimal_access }
+ it { is_expected.not_to include awaiting_group_member }
+ it { is_expected.not_to include awaiting_project_member }
+ end
end
describe '.connected_to_user' do
diff --git a/spec/models/members/members/members_with_parents_spec.rb b/spec/models/members/members/members_with_parents_spec.rb
new file mode 100644
index 00000000000..46c934c932f
--- /dev/null
+++ b/spec/models/members/members/members_with_parents_spec.rb
@@ -0,0 +1,92 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Members::MembersWithParents, feature_category: :groups_and_projects do
+ let_it_be(:group) { create(:group, :nested) }
+ let_it_be(:maintainer) { group.parent.add_maintainer(create(:user)) }
+ let_it_be(:developer) { group.add_developer(create(:user)) }
+ let_it_be(:pending_maintainer) { create(:group_member, :awaiting, :maintainer, group: group.parent) }
+ let_it_be(:pending_developer) { create(:group_member, :awaiting, :developer, group: group) }
+ let_it_be(:invited_member) { create(:group_member, :invited, group: group) }
+ let_it_be(:inactive_developer) { group.add_developer(create(:user, :deactivated)) }
+ let_it_be(:minimal_access) { create(:group_member, :minimal_access, group: group) }
+
+ describe '#all_members' do
+ subject(:all_members) { described_class.new(group).all_members }
+
+ it 'returns all members for group and group parents' do
+ expect(all_members).to contain_exactly(
+ developer,
+ maintainer,
+ pending_maintainer,
+ pending_developer,
+ invited_member,
+ inactive_developer,
+ minimal_access
+ )
+ end
+ end
+
+ describe '#members' do
+ let(:arguments) { {} }
+
+ subject(:members) { described_class.new(group).members(**arguments) }
+
+ using Rspec::Parameterized::TableSyntax
+
+ where(:arguments, :expected_members) do
+ [
+ [
+ {},
+ lazy { [developer, maintainer, inactive_developer] }
+ ],
+ [
+ # minimal access is Premium, so in FOSS we will not include minimal access member
+ { minimal_access: true },
+ lazy { [developer, maintainer, inactive_developer] }
+ ],
+ [
+ { active_users: true },
+ lazy { [developer, maintainer] }
+ ]
+ ]
+ end
+
+ with_them do
+ it 'returns expected members' do
+ expect(members).to contain_exactly(*expected_members)
+ expect(members).not_to include(*(group.members - expected_members))
+ end
+ end
+
+ context 'when active_users: true and minimal_access: true' do
+ let(:arguments) { { active_users: true, minimal_access: true } }
+
+ it 'raises an error' do
+ expect { members }.to raise_error(ArgumentError)
+ end
+ end
+
+ context 'with group sharing' do
+ let_it_be(:shared_with_group) { create(:group) }
+
+ let_it_be(:shared_with_group_maintainer) do
+ shared_with_group.add_maintainer(create(:user))
+ end
+
+ let_it_be(:shared_with_group_developer) do
+ shared_with_group.add_developer(create(:user))
+ end
+
+ before do
+ create(:group_group_link, shared_group: group, shared_with_group: shared_with_group)
+ end
+
+ it 'returns shared with group members' do
+ expect(members).to(include(shared_with_group_maintainer))
+ expect(members).to(include(shared_with_group_developer))
+ end
+ end
+ end
+end
diff --git a/spec/models/members/project_member_spec.rb b/spec/models/members/project_member_spec.rb
index a2b5bde8890..a9725a796bf 100644
--- a/spec/models/members/project_member_spec.rb
+++ b/spec/models/members/project_member_spec.rb
@@ -51,6 +51,50 @@ RSpec.describe ProjectMember, feature_category: :groups_and_projects do
end
end
+ describe '.permissible_access_level_roles_for_project_access_token' do
+ let_it_be(:owner) { create(:user) }
+ let_it_be(:maintainer) { create(:user) }
+ let_it_be(:developer) { create(:user) }
+ let_it_be(:group) { create(:group) }
+ let_it_be(:project) { create(:project, group: group) }
+
+ before do
+ project.add_owner(owner)
+ project.add_maintainer(maintainer)
+ project.add_developer(developer)
+ end
+
+ subject(:access_levels) { described_class.permissible_access_level_roles_for_project_access_token(user, project) }
+
+ context 'when member can manage owners' do
+ let(:user) { owner }
+
+ it 'returns Gitlab::Access.options_with_owner' do
+ expect(access_levels).to eq(Gitlab::Access.options_with_owner)
+ end
+ end
+
+ context 'when member cannot manage owners' do
+ let(:user) { maintainer }
+
+ it 'returns Gitlab::Access.options' do
+ expect(access_levels).to eq(Gitlab::Access.options)
+ end
+ end
+
+ context 'when the user is a developer' do
+ let(:user) { developer }
+
+ it 'returns Gitlab::Access.options' do
+ expect(access_levels).to eq({
+ "Guest" => 10,
+ "Reporter" => 20,
+ "Developer" => 30
+ })
+ end
+ end
+ end
+
describe '#real_source_type' do
subject { create(:project_member).real_source_type }
diff --git a/spec/models/merge_request_diff_spec.rb b/spec/models/merge_request_diff_spec.rb
index 806ce3f21b5..bcab2029942 100644
--- a/spec/models/merge_request_diff_spec.rb
+++ b/spec/models/merge_request_diff_spec.rb
@@ -1090,7 +1090,7 @@ RSpec.describe MergeRequestDiff, feature_category: :code_review_workflow do
end
it 'returns affected file paths' do
- expect(subject.modified_paths).to eq(%w{foo bar baz})
+ expect(subject.modified_paths).to eq(%w[foo bar baz])
end
context "when fallback_on_overflow is true" do
diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb
index 40f85c92851..d3c32da2842 100644
--- a/spec/models/merge_request_spec.rb
+++ b/spec/models/merge_request_spec.rb
@@ -725,22 +725,35 @@ RSpec.describe MergeRequest, factory_default: :keep, feature_category: :code_rev
end
end
- describe '.recent_target_branches' do
+ describe '.recent_target_branches and .recent_source_branches' do
+ def create_mr(source_branch, target_branch, status, remove_source_branch = false)
+ if remove_source_branch
+ create(:merge_request, status, :remove_source_branch, source_project: project,
+ target_branch: target_branch, source_branch: source_branch)
+ else
+ create(:merge_request, status, source_project: project,
+ target_branch: target_branch, source_branch: source_branch)
+ end
+ end
+
let(:project) { create(:project) }
- let!(:merge_request1) { create(:merge_request, :opened, source_project: project, target_branch: 'feature') }
- let!(:merge_request2) { create(:merge_request, :closed, source_project: project, target_branch: 'merge-test') }
- let!(:merge_request3) { create(:merge_request, :opened, source_project: project, target_branch: 'fix') }
- let!(:merge_request4) { create(:merge_request, :closed, source_project: project, target_branch: 'feature') }
+ let!(:merge_request1) { create_mr('source1', 'target1', :opened) }
+ let!(:merge_request2) { create_mr('source2', 'target2', :closed) }
+ let!(:merge_request3) { create_mr('source3', 'target3', :opened) }
+ let!(:merge_request4) { create_mr('source4', 'target1', :closed) }
+ let!(:merge_request5) { create_mr('source5', 'target4', :merged, true) }
before do
merge_request1.update_columns(updated_at: 1.day.since)
merge_request2.update_columns(updated_at: 2.days.since)
merge_request3.update_columns(updated_at: 3.days.since)
merge_request4.update_columns(updated_at: 4.days.since)
+ merge_request5.update_columns(updated_at: 5.days.since)
end
- it 'returns target branches sort by updated at desc' do
- expect(described_class.recent_target_branches).to match_array(%w[feature merge-test fix])
+ it 'returns branches sort by updated at desc' do
+ expect(described_class.recent_target_branches).to match_array(%w[target1 target2 target3 target4])
+ expect(described_class.recent_source_branches).to match_array(%w[source1 source2 source3 source4 source5])
end
end
@@ -3324,6 +3337,7 @@ RSpec.describe MergeRequest, factory_default: :keep, feature_category: :code_rev
context 'with skip_ci_check option' do
before do
+ allow(subject.project).to receive(:only_allow_merge_if_pipeline_succeeds?).and_return(true)
allow(subject).to receive_messages(check_mergeability: nil, can_be_merged?: true, broken?: false)
end
@@ -3345,6 +3359,8 @@ RSpec.describe MergeRequest, factory_default: :keep, feature_category: :code_rev
context 'with skip_discussions_check option' do
before do
+ allow(subject.project).to receive(:only_allow_merge_if_all_discussions_are_resolved?).and_return(true)
+
allow(subject).to receive_messages(
mergeable_ci_state?: true,
check_mergeability: nil,
@@ -3380,6 +3396,8 @@ RSpec.describe MergeRequest, factory_default: :keep, feature_category: :code_rev
context 'with skip_rebase_check option' do
before do
+ allow(subject.project).to receive(:ff_merge_must_be_possible?).and_return(true)
+
allow(subject).to receive_messages(
mergeable_state?: true,
check_mergeability: nil,
@@ -3525,6 +3543,7 @@ RSpec.describe MergeRequest, factory_default: :keep, feature_category: :code_rev
context 'when failed' do
context 'when #mergeable_ci_state? is false' do
before do
+ allow(subject.project).to receive(:only_allow_merge_if_pipeline_succeeds?) { true }
allow(subject).to receive(:mergeable_ci_state?) { false }
end
@@ -3539,6 +3558,7 @@ RSpec.describe MergeRequest, factory_default: :keep, feature_category: :code_rev
context 'when #mergeable_discussions_state? is false' do
before do
+ allow(subject.project).to receive(:only_allow_merge_if_all_discussions_are_resolved?) { true }
allow(subject).to receive(:mergeable_discussions_state?) { false }
end
@@ -6016,12 +6036,10 @@ RSpec.describe MergeRequest, factory_default: :keep, feature_category: :code_rev
allow(merge_request_diff).to receive(:patch_id_sha).and_return(nil)
allow(merge_request).to receive(:diff_refs).and_return(diff_refs)
- allow_next_instance_of(Repository) do |repo|
- allow(repo)
- .to receive(:get_patch_id)
- .with(diff_refs.base_sha, diff_refs.head_sha)
- .and_return(patch_id)
- end
+ allow(merge_request.project.repository)
+ .to receive(:get_patch_id)
+ .with(diff_refs.base_sha, diff_refs.head_sha)
+ .and_return(patch_id)
end
it { is_expected.to eq(patch_id) }
@@ -6065,4 +6083,54 @@ RSpec.describe MergeRequest, factory_default: :keep, feature_category: :code_rev
expect(merge_request.all_mergeability_checks_results).to eq(result.payload[:results])
end
end
+
+ describe '#only_allow_merge_if_pipeline_succeeds?' do
+ let(:merge_request) { build_stubbed(:merge_request) }
+
+ subject(:result) { merge_request.only_allow_merge_if_pipeline_succeeds? }
+
+ before do
+ allow(merge_request.project)
+ .to receive(:only_allow_merge_if_pipeline_succeeds?)
+ .with(inherit_group_setting: true)
+ .and_return(only_allow_merge_if_pipeline_succeeds?)
+ end
+
+ context 'when associated project only_allow_merge_if_pipeline_succeeds? returns true' do
+ let(:only_allow_merge_if_pipeline_succeeds?) { true }
+
+ it { is_expected.to eq(true) }
+ end
+
+ context 'when associated project only_allow_merge_if_pipeline_succeeds? returns false' do
+ let(:only_allow_merge_if_pipeline_succeeds?) { false }
+
+ it { is_expected.to eq(false) }
+ end
+ end
+
+ describe '#only_allow_merge_if_all_discussions_are_resolved?' do
+ let(:merge_request) { build_stubbed(:merge_request) }
+
+ subject(:result) { merge_request.only_allow_merge_if_all_discussions_are_resolved? }
+
+ before do
+ allow(merge_request.project)
+ .to receive(:only_allow_merge_if_all_discussions_are_resolved?)
+ .with(inherit_group_setting: true)
+ .and_return(only_allow_merge_if_all_discussions_are_resolved?)
+ end
+
+ context 'when associated project only_allow_merge_if_all_discussions_are_resolved? returns true' do
+ let(:only_allow_merge_if_all_discussions_are_resolved?) { true }
+
+ it { is_expected.to eq(true) }
+ end
+
+ context 'when associated project only_allow_merge_if_all_discussions_are_resolved? returns false' do
+ let(:only_allow_merge_if_all_discussions_are_resolved?) { false }
+
+ it { is_expected.to eq(false) }
+ end
+ end
end
diff --git a/spec/models/ml/candidate_spec.rb b/spec/models/ml/candidate_spec.rb
index fa19b723ee2..d5b71e2c3f7 100644
--- a/spec/models/ml/candidate_spec.rb
+++ b/spec/models/ml/candidate_spec.rb
@@ -16,6 +16,7 @@ RSpec.describe Ml::Candidate, factory_default: :keep, feature_category: :mlops d
it { is_expected.to belong_to(:user) }
it { is_expected.to belong_to(:package) }
it { is_expected.to belong_to(:ci_build).class_name('Ci::Build') }
+ it { is_expected.to belong_to(:model_version).class_name('Ml::ModelVersion') }
it { is_expected.to have_many(:params) }
it { is_expected.to have_many(:metrics) }
it { is_expected.to have_many(:metadata) }
@@ -35,6 +36,45 @@ RSpec.describe Ml::Candidate, factory_default: :keep, feature_category: :mlops d
it { expect(described_class.new.eid).to be_present }
end
+ describe 'validation' do
+ let_it_be(:model) { create(:ml_models, project: candidate.project) }
+ let_it_be(:model_version1) { create(:ml_model_versions, model: model) }
+ let_it_be(:model_version2) { create(:ml_model_versions, model: model) }
+ let_it_be(:validation_candidate) do
+ create(:ml_candidates, model_version: model_version1, project: candidate.project)
+ end
+
+ let(:params) do
+ {
+ model_version: nil
+ }
+ end
+
+ subject(:errors) do
+ candidate = described_class.new(**params)
+ candidate.validate
+ candidate.errors
+ end
+
+ describe 'model_version' do
+ context 'when model_version is nil' do
+ it { expect(errors).not_to include(:model_version_id) }
+ end
+
+ context 'when no other candidate is associated to the model_version' do
+ let(:params) { { model_version: model_version2 } }
+
+ it { expect(errors).not_to include(:model_version_id) }
+ end
+
+ context 'when another candidate has model_version_id' do
+ let(:params) { { model_version: validation_candidate.model_version } }
+
+ it { expect(errors).to include(:model_version_id) }
+ end
+ end
+ end
+
describe '.destroy' do
let_it_be(:candidate_to_destroy) do
create(:ml_candidates, :with_metrics_and_params, :with_metadata, :with_artifact)
diff --git a/spec/models/ml/model_metadata_spec.rb b/spec/models/ml/model_metadata_spec.rb
new file mode 100644
index 00000000000..f06c7a2ce50
--- /dev/null
+++ b/spec/models/ml/model_metadata_spec.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Ml::ModelMetadata, feature_category: :mlops do
+ describe 'associations' do
+ it { is_expected.to belong_to(:model).required }
+ end
+
+ describe 'validations' do
+ let_it_be(:metadata) { create(:ml_model_metadata, name: 'some_metadata') }
+ let_it_be(:model) { metadata.model }
+
+ it 'is unique within the model' do
+ expect do
+ model.metadata.create!(name: 'some_metadata', value: 'blah')
+ end.to raise_error.with_message(/Name 'some_metadata' already taken/)
+ end
+
+ it 'a model is required' do
+ expect do
+ described_class.create!(name: 'some_metadata', value: 'blah')
+ end.to raise_error.with_message(/Model must exist/)
+ end
+ end
+end
diff --git a/spec/models/ml/model_spec.rb b/spec/models/ml/model_spec.rb
index e22989f3ce2..ae7c3f163f3 100644
--- a/spec/models/ml/model_spec.rb
+++ b/spec/models/ml/model_spec.rb
@@ -14,6 +14,7 @@ RSpec.describe Ml::Model, feature_category: :mlops do
it { is_expected.to belong_to(:project) }
it { is_expected.to have_one(:default_experiment) }
it { is_expected.to have_many(:versions) }
+ it { is_expected.to have_many(:metadata) }
it { is_expected.to have_one(:latest_version).class_name('Ml::ModelVersion').inverse_of(:model) }
end
@@ -77,45 +78,11 @@ RSpec.describe Ml::Model, feature_category: :mlops do
end
end
- describe '.find_or_create' do
- subject(:find_or_create) { described_class.find_or_create(project, name, experiment) }
+ describe '.including_project' do
+ subject { described_class.including_project }
- let(:name) { existing_model.name }
- let(:project) { existing_model.project }
- let(:experiment) { default_experiment }
-
- context 'when model name does not exist in the project' do
- let(:name) { 'new_model' }
- let(:experiment) { build(:ml_experiments, name: name, project: project) }
-
- it 'creates a model', :aggregate_failures do
- expect { find_or_create }.to change { Ml::Model.count }.by(1)
-
- expect(find_or_create.name).to eq(name)
- expect(find_or_create.project).to eq(project)
- expect(find_or_create.default_experiment).to eq(experiment)
- end
- end
-
- context 'when model name exists but project is different' do
- let(:project) { create(:project) }
- let(:experiment) { build(:ml_experiments, name: name, project: project) }
-
- it 'creates a model', :aggregate_failures do
- expect { find_or_create }.to change { Ml::Model.count }.by(1)
-
- expect(find_or_create.name).to eq(name)
- expect(find_or_create.project).to eq(project)
- expect(find_or_create.default_experiment).to eq(experiment)
- end
- end
-
- context 'when model exists' do
- it 'fetches existing model', :aggregate_failures do
- expect { find_or_create }.not_to change { Ml::Model.count }
-
- expect(find_or_create).to eq(existing_model)
- end
+ it 'loads latest version' do
+ expect(subject.first.association_cached?(:project)).to be(true)
end
end
diff --git a/spec/models/ml/model_version_spec.rb b/spec/models/ml/model_version_spec.rb
index 83639fca9e1..95d4a545f52 100644
--- a/spec/models/ml/model_version_spec.rb
+++ b/spec/models/ml/model_version_spec.rb
@@ -18,6 +18,7 @@ RSpec.describe Ml::ModelVersion, feature_category: :mlops do
it { is_expected.to belong_to(:project) }
it { is_expected.to belong_to(:model) }
it { is_expected.to belong_to(:package).class_name('Packages::MlModel::Package') }
+ it { is_expected.to have_one(:candidate).class_name('Ml::Candidate') }
end
describe 'validation' do
@@ -26,11 +27,15 @@ RSpec.describe Ml::ModelVersion, feature_category: :mlops do
build_stubbed(:ml_model_package, project: base_project, version: valid_version, name: model1.name)
end
+ let_it_be(:valid_description) { 'Valid description' }
+
let(:package) { valid_package }
let(:version) { valid_version }
+ let(:description) { valid_description }
subject(:errors) do
- mv = described_class.new(version: version, model: model1, package: package, project: model1.project)
+ mv = described_class.new(version: version, model: model1, package: package, project: model1.project,
+ description: description)
mv.validate
mv.errors
end
@@ -60,6 +65,14 @@ RSpec.describe Ml::ModelVersion, feature_category: :mlops do
end
end
+ describe 'description' do
+ context 'when description is too large' do
+ let(:description) { 'a' * 501 }
+
+ it { expect(errors).to include(:description) }
+ end
+ end
+
describe 'model' do
context 'when project is different' do
before do
@@ -91,8 +104,9 @@ RSpec.describe Ml::ModelVersion, feature_category: :mlops do
let(:version) { existing_model_version.version }
let(:package) { nil }
+ let(:description) { 'Some description' }
- subject(:find_or_create) { described_class.find_or_create!(model1, version, package) }
+ subject(:find_or_create) { described_class.find_or_create!(model1, version, package, description) }
context 'if model version exists' do
it 'returns the model version', :aggregate_failures do
@@ -111,11 +125,66 @@ RSpec.describe Ml::ModelVersion, feature_category: :mlops do
expect(model_version.version).to eq(version)
expect(model_version.model).to eq(model1)
+ expect(model_version.description).to eq(description)
expect(model_version.package).to eq(package)
end
end
end
+ describe '#by_project_id_and_id' do
+ let(:id) { model_version1.id }
+ let(:project_id) { model_version1.project.id }
+
+ subject { described_class.by_project_id_and_id(project_id, id) }
+
+ context 'if exists' do
+ it { is_expected.to eq(model_version1) }
+ end
+
+ context 'if id has no match' do
+ let(:id) { non_existing_record_id }
+
+ it { is_expected.to be(nil) }
+ end
+
+ context 'if project id does not match' do
+ let(:project_id) { non_existing_record_id }
+
+ it { is_expected.to be(nil) }
+ end
+ end
+
+ describe '.by_project_id_name_and_version' do
+ let(:version) { model_version1.version }
+ let(:project_id) { model_version1.project.id }
+ let(:model_name) { model_version1.model.name }
+ let_it_be(:latest_version) { create(:ml_model_versions, model: model_version1.model) }
+
+ subject { described_class.by_project_id_name_and_version(project_id, model_name, version) }
+
+ context 'if exists' do
+ it { is_expected.to eq(model_version1) }
+ end
+
+ context 'if id has no match' do
+ let(:version) { non_existing_record_id }
+
+ it { is_expected.to be(nil) }
+ end
+
+ context 'if project id does not match' do
+ let(:project_id) { non_existing_record_id }
+
+ it { is_expected.to be(nil) }
+ end
+
+ context 'if model name does not match' do
+ let(:model_name) { non_existing_record_id }
+
+ it { is_expected.to be(nil) }
+ end
+ end
+
describe '.order_by_model_id_id_desc' do
subject { described_class.order_by_model_id_id_desc }
diff --git a/spec/models/namespace_setting_spec.rb b/spec/models/namespace_setting_spec.rb
index e9822d97447..c7449e047b0 100644
--- a/spec/models/namespace_setting_spec.rb
+++ b/spec/models/namespace_setting_spec.rb
@@ -216,32 +216,54 @@ RSpec.describe NamespaceSetting, feature_category: :groups_and_projects, type: :
end
end
- describe '#emails_enabled?' do
- context 'when a group has no parent'
- let(:settings) { create(:namespace_settings, emails_enabled: true) }
- let(:grandparent) { create(:group) }
+ context 'when a group has parent groups' do
+ let(:grandparent) { create(:group, namespace_settings: settings) }
let(:parent) { create(:group, parent: grandparent) }
- let(:group) { create(:group, parent: parent, namespace_settings: settings) }
+ let!(:group) { create(:group, parent: parent) }
- context 'when the groups setting is changed' do
- it 'returns false when the attribute is false' do
- group.update_attribute(:emails_disabled, true)
+ context "when a parent group has disabled diff previews" do
+ let(:settings) { create(:namespace_settings, show_diff_preview_in_email: false) }
- expect(group.emails_enabled?).to be_falsey
+ it 'returns false' do
+ expect(group.show_diff_preview_in_email?).to be_falsey
end
end
- context 'when a group has a parent' do
- it 'returns true when no parent has disabled emails' do
- expect(group.emails_enabled?).to be_truthy
+ context 'when all parent groups have enabled diff previews' do
+ let(:settings) { create(:namespace_settings, show_diff_preview_in_email: true) }
+
+ it 'returns true' do
+ expect(group.show_diff_preview_in_email?).to be_truthy
end
+ end
+ end
+ end
- context 'when ancestor emails are disabled' do
- it 'returns false' do
- grandparent.update_attribute(:emails_disabled, true)
+ describe '#emails_enabled?' do
+ context 'when a group has no parent'
+ let(:settings) { create(:namespace_settings, emails_enabled: true) }
+ let(:grandparent) { create(:group) }
+ let(:parent) { create(:group, parent: grandparent) }
+ let(:group) { create(:group, parent: parent, namespace_settings: settings) }
- expect(group.emails_enabled?).to be_falsey
- end
+ context 'when the groups setting is changed' do
+ it 'returns false when the attribute is false' do
+ group.update_attribute(:emails_enabled, false)
+
+ expect(group.emails_enabled?).to be_falsey
+ end
+ end
+
+ context 'when a group has a parent' do
+ it 'returns true when no parent has disabled emails' do
+ expect(group.emails_enabled?).to be_truthy
+ end
+
+ context 'when ancestor emails are disabled' do
+ it 'returns false' do
+ grandparent.update_attribute(:emails_enabled, false)
+
+ expect(group.emails_enabled?).to be_falsey
end
end
end
@@ -251,19 +273,19 @@ RSpec.describe NamespaceSetting, feature_category: :groups_and_projects, type: :
let(:parent) { create(:group, parent: grandparent) }
let!(:group) { create(:group, parent: parent) }
- context "when a parent group has disabled diff previews" do
- let(:settings) { create(:namespace_settings, show_diff_preview_in_email: false) }
+ context "when a parent group has emails disabled" do
+ let(:settings) { create(:namespace_settings, emails_enabled: false) }
it 'returns false' do
- expect(group.show_diff_preview_in_email?).to be_falsey
+ expect(group.emails_enabled?).to be_falsey
end
end
- context 'when all parent groups have enabled diff previews' do
- let(:settings) { create(:namespace_settings, show_diff_preview_in_email: true) }
+ context 'when all parent groups have emails enabled' do
+ let(:settings) { create(:namespace_settings, emails_enabled: true) }
it 'returns true' do
- expect(group.show_diff_preview_in_email?).to be_truthy
+ expect(group.emails_enabled?).to be_truthy
end
end
end
diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb
index 9974aac3c6c..85569a68252 100644
--- a/spec/models/namespace_spec.rb
+++ b/spec/models/namespace_spec.rb
@@ -569,6 +569,48 @@ RSpec.describe Namespace, feature_category: :groups_and_projects do
end
end
end
+
+ describe "#default_branch_protection_settings" do
+ let(:default_branch_protection_defaults) { {} }
+ let(:namespace_setting) { create(:namespace_settings, default_branch_protection_defaults: default_branch_protection_defaults) }
+ let(:namespace) { create(:namespace, namespace_settings: namespace_setting) }
+ let(:group) { create(:group, namespace_settings: namespace_setting) }
+
+ before do
+ stub_application_setting(default_branch_protection_defaults: Gitlab::Access::BranchProtection.protected_against_developer_pushes)
+ end
+
+ context 'for a namespace' do
+ it 'returns the instance level setting' do
+ expected_settings = Gitlab::Access::BranchProtection.protected_against_developer_pushes.deep_stringify_keys
+ settings = namespace.default_branch_protection_settings.to_hash
+
+ expect(settings).to eq(expected_settings)
+ end
+ end
+
+ context 'for a group' do
+ context 'that has not altered the default value' do
+ it 'returns the instance level setting' do
+ expected_settings = Gitlab::Access::BranchProtection.protected_against_developer_pushes.deep_stringify_keys
+ settings = group.default_branch_protection_settings.to_hash
+
+ expect(settings).to eq(expected_settings)
+ end
+ end
+
+ context 'that has altered the default value' do
+ let(:default_branch_protection_defaults) { Gitlab::Access::BranchProtection.protected_fully.deep_stringify_keys }
+
+ it 'returns the group level setting' do
+ expected_settings = default_branch_protection_defaults
+ settings = group.default_branch_protection_settings.to_hash
+
+ expect(settings).to eq(expected_settings)
+ end
+ end
+ end
+ end
end
describe "Respond to" do
@@ -1863,20 +1905,22 @@ RSpec.describe Namespace, feature_category: :groups_and_projects do
describe '#emails_disabled?' do
context 'when not a subgroup' do
+ let(:group) { create(:group) }
+
it 'returns false' do
- group = create(:group, emails_disabled: false)
+ group.update_attribute(:emails_enabled, true)
expect(group.emails_disabled?).to be_falsey
end
it 'returns true' do
- group = create(:group, emails_disabled: true)
+ group.update_attribute(:emails_enabled, false)
expect(group.emails_disabled?).to be_truthy
end
it 'does not query the db when there is no parent group' do
- group = create(:group, emails_disabled: true)
+ group.update_attribute(:emails_enabled, false)
expect { group.emails_disabled? }.not_to exceed_query_limit(0)
end
@@ -1903,7 +1947,8 @@ RSpec.describe Namespace, feature_category: :groups_and_projects do
describe '#emails_enabled?' do
context 'without a persisted namespace_setting object' do
- let(:group) { build(:group, emails_disabled: false) }
+ let(:group_settings) { create(:namespace_settings) }
+ let(:group) { build(:group, emails_disabled: false, namespace_settings: group_settings) }
it "is the opposite of emails_disabled" do
expect(group.emails_enabled?).to be_truthy
@@ -1928,7 +1973,7 @@ RSpec.describe Namespace, feature_category: :groups_and_projects do
parent_2 = create(:group) # No projects
create(:project, group: child_1_1).tap do |project|
- project.pages_metadatum.update!(deployed: true)
+ create(:pages_deployment, project: project)
end
create(:project, group: child_1_1)
diff --git a/spec/models/namespace_statistics_spec.rb b/spec/models/namespace_statistics_spec.rb
index ac747b70a9f..ee3b4765dba 100644
--- a/spec/models/namespace_statistics_spec.rb
+++ b/spec/models/namespace_statistics_spec.rb
@@ -171,7 +171,7 @@ RSpec.describe NamespaceStatistics do
context 'when other columns are updated' do
it 'does not enqueue the job to update root storage statistics' do
- columns_to_update = NamespaceStatistics.columns_hash.reject { |k, _| %w(id namespace_id).include?(k) || k.include?('_size') }.keys
+ columns_to_update = NamespaceStatistics.columns_hash.reject { |k, _| %w[id namespace_id].include?(k) || k.include?('_size') }.keys
columns_to_update.each { |c| statistics[c] = 10 }
expect(statistics).not_to receive(:update_root_storage_statistics)
diff --git a/spec/models/network/graph_spec.rb b/spec/models/network/graph_spec.rb
index d0c73d6285c..3bee7225df5 100644
--- a/spec/models/network/graph_spec.rb
+++ b/spec/models/network/graph_spec.rb
@@ -4,7 +4,6 @@ require 'spec_helper'
RSpec.describe Network::Graph, feature_category: :source_code_management do
let(:project) { create(:project, :repository) }
- let!(:note_on_commit) { create(:note_on_commit, project: project) }
describe '#initialize' do
let(:graph) do
@@ -14,16 +13,6 @@ RSpec.describe Network::Graph, feature_category: :source_code_management do
it 'has initialized' do
expect(graph).to be_a(described_class)
end
-
- context 'when disable_network_graph_note_counts is disabled' do
- before do
- stub_feature_flags(disable_network_graph_notes_count: false)
- end
-
- it 'initializes the notes hash' do
- expect(graph.notes).to eq({ note_on_commit.commit_id => 1 })
- end
- end
end
describe '#commits' do
diff --git a/spec/models/notification_recipient_spec.rb b/spec/models/notification_recipient_spec.rb
index 2275bea4c7f..f19c0a68f87 100644
--- a/spec/models/notification_recipient_spec.rb
+++ b/spec/models/notification_recipient_spec.rb
@@ -14,7 +14,7 @@ RSpec.describe NotificationRecipient, feature_category: :team_planning do
context 'when emails are disabled' do
it 'returns false if group disabled' do
- expect(project.namespace).to receive(:emails_disabled?).and_return(true)
+ expect(project.namespace).to receive(:emails_enabled?).and_return(false)
expect(recipient).to receive(:emails_disabled?).and_call_original
expect(recipient.notifiable?).to eq false
end
@@ -28,7 +28,7 @@ RSpec.describe NotificationRecipient, feature_category: :team_planning do
context 'when emails are enabled' do
it 'returns true if group enabled' do
- expect(project.namespace).to receive(:emails_disabled?).and_return(false)
+ expect(project.namespace).to receive(:emails_enabled?).and_return(true)
expect(recipient).to receive(:emails_disabled?).and_call_original
expect(recipient.notifiable?).to eq true
end
diff --git a/spec/models/organizations/organization_spec.rb b/spec/models/organizations/organization_spec.rb
index 2f9f04fd3e6..0670002135c 100644
--- a/spec/models/organizations/organization_spec.rb
+++ b/spec/models/organizations/organization_spec.rb
@@ -181,4 +181,13 @@ RSpec.describe Organizations::Organization, type: :model, feature_category: :cel
it { is_expected.to eq false }
end
end
+
+ describe '#web_url' do
+ it 'returns web url from `Gitlab::UrlBuilder`' do
+ web_url = 'http://127.0.0.1:3000/-/organizations/default'
+
+ expect(Gitlab::UrlBuilder).to receive(:build).with(organization, only_path: nil).and_return(web_url)
+ expect(organization.web_url).to eq(web_url)
+ end
+ end
end
diff --git a/spec/models/packages/npm/metadata_cache_spec.rb b/spec/models/packages/npm/metadata_cache_spec.rb
index 94b41ab6a5e..3a6c87a4244 100644
--- a/spec/models/packages/npm/metadata_cache_spec.rb
+++ b/spec/models/packages/npm/metadata_cache_spec.rb
@@ -148,4 +148,40 @@ RSpec.describe Packages::Npm::MetadataCache, type: :model, feature_category: :pa
end
end
end
+
+ describe '.stale' do
+ let_it_be(:npm_metadata_cache) { create(:npm_metadata_cache) }
+ let_it_be(:npm_metadata_cache_stale) { create(:npm_metadata_cache, :stale) }
+
+ subject { described_class.stale }
+
+ it { is_expected.to contain_exactly(npm_metadata_cache_stale) }
+ end
+
+ describe '.pending_destruction' do
+ let_it_be(:npm_metadata_cache) { create(:npm_metadata_cache) }
+ let_it_be(:npm_metadata_cache_stale_default) { create(:npm_metadata_cache, :stale) }
+ let_it_be(:npm_metadata_cache_stale_processing) { create(:npm_metadata_cache, :stale, :processing) }
+
+ subject { described_class.pending_destruction }
+
+ it { is_expected.to contain_exactly(npm_metadata_cache_stale_default) }
+ end
+
+ describe '.next_pending_destruction' do
+ let_it_be(:npm_metadata_cache1) { create(:npm_metadata_cache, created_at: 1.month.ago, updated_at: 1.day.ago) }
+ let_it_be(:npm_metadata_cache2) { create(:npm_metadata_cache, created_at: 1.year.ago, updated_at: 1.year.ago) }
+
+ let_it_be(:npm_metadata_cache3) do
+ create(:npm_metadata_cache, :stale, created_at: 2.years.ago, updated_at: 1.month.ago)
+ end
+
+ let_it_be(:npm_metadata_cache4) do
+ create(:npm_metadata_cache, :stale, created_at: 3.years.ago, updated_at: 2.weeks.ago)
+ end
+
+ it 'returns the oldest pending destruction item based on updated_at' do
+ expect(described_class.next_pending_destruction(order_by: :updated_at)).to eq(npm_metadata_cache3)
+ end
+ end
end
diff --git a/spec/models/packages/nuget/symbol_spec.rb b/spec/models/packages/nuget/symbol_spec.rb
index 52e95c11939..f43f3a3bdeb 100644
--- a/spec/models/packages/nuget/symbol_spec.rb
+++ b/spec/models/packages/nuget/symbol_spec.rb
@@ -6,6 +6,7 @@ RSpec.describe Packages::Nuget::Symbol, type: :model, feature_category: :package
subject(:symbol) { create(:nuget_symbol) }
it { is_expected.to be_a FileStoreMounter }
+ it { is_expected.to be_a ShaAttribute }
describe 'relationships' do
it { is_expected.to belong_to(:package).inverse_of(:nuget_symbols) }
diff --git a/spec/models/packages/package_spec.rb b/spec/models/packages/package_spec.rb
index e113218e828..8e3b97e55f3 100644
--- a/spec/models/packages/package_spec.rb
+++ b/spec/models/packages/package_spec.rb
@@ -1196,7 +1196,7 @@ RSpec.describe Packages::Package, type: :model, feature_category: :package_regis
it { is_expected.to eq([]) }
context 'with tags' do
- let(:tags) { %w(tag1 tag2 tag3) }
+ let(:tags) { %w[tag1 tag2 tag3] }
before do
tags.each { |t| create(:packages_tag, name: t, package: package) }
diff --git a/spec/models/packages/protection/rule_spec.rb b/spec/models/packages/protection/rule_spec.rb
index 320c265239c..3f0aefa945a 100644
--- a/spec/models/packages/protection/rule_spec.rb
+++ b/spec/models/packages/protection/rule_spec.rb
@@ -34,6 +34,32 @@ RSpec.describe Packages::Protection::Rule, type: :model, feature_category: :pack
it { is_expected.to validate_presence_of(:package_name_pattern) }
it { is_expected.to validate_uniqueness_of(:package_name_pattern).scoped_to(:project_id, :package_type) }
it { is_expected.to validate_length_of(:package_name_pattern).is_at_most(255) }
+
+ [
+ '@my-scope/my-package',
+ '@my-scope/*my-package-with-wildcard-inbetween',
+ '@my-scope/*my-package-with-wildcard-start',
+ '@my-scope/my-*package-*with-wildcard-multiple-*',
+ '@my-scope/my-package-with_____underscore',
+ '@my-scope/my-package-with-regex-characters.+',
+ '@my-scope/my-package-with-wildcard-end*'
+ ].each do |package_name_pattern|
+ it { is_expected.to allow_value(package_name_pattern).for(:package_name_pattern) }
+ end
+
+ [
+ '@my-scope/my-package-with-percent-sign-%',
+ '*@my-scope/my-package-with-wildcard-start',
+ '@my-scope/my-package-with-backslash-\*'
+ ].each do |package_name_pattern|
+ it {
+ is_expected.not_to(
+ allow_value(package_name_pattern)
+ .for(:package_name_pattern)
+ .with_message(_('should be a valid NPM package name with optional wildcard characters.'))
+ )
+ }
+ end
end
describe '#package_type' do
@@ -51,14 +77,13 @@ RSpec.describe Packages::Protection::Rule, type: :model, feature_category: :pack
context 'with different package name patterns' do
where(:package_name_pattern, :expected_pattern_query) do
- '@my-scope/my-package' | '@my-scope/my-package'
- '*@my-scope/my-package-with-wildcard-start' | '%@my-scope/my-package-with-wildcard-start'
- '@my-scope/my-package-with-wildcard-end*' | '@my-scope/my-package-with-wildcard-end%'
- '@my-scope/*my-package-with-wildcard-inbetween' | '@my-scope/%my-package-with-wildcard-inbetween'
- '**@my-scope/**my-package-with-wildcard-multiple**' | '%%@my-scope/%%my-package-with-wildcard-multiple%%'
- '@my-scope/my-package-with_____underscore' | '@my-scope/my-package-with\_\_\_\_\_underscore'
- '@my-scope/my-package-with-percent-sign-%' | '@my-scope/my-package-with-percent-sign-\%'
- '@my-scope/my-package-with-regex-characters.+' | '@my-scope/my-package-with-regex-characters.+'
+ '@my-scope/my-package' | '@my-scope/my-package'
+ '@my-scope/*my-package-with-wildcard-start' | '@my-scope/%my-package-with-wildcard-start'
+ '@my-scope/my-package-with-wildcard-end*' | '@my-scope/my-package-with-wildcard-end%'
+ '@my-scope/my-package*with-wildcard-inbetween' | '@my-scope/my-package%with-wildcard-inbetween'
+ '@my-scope/**my-package-**-with-wildcard-multiple**' | '@my-scope/%%my-package-%%-with-wildcard-multiple%%'
+ '@my-scope/my-package-with_____underscore' | '@my-scope/my-package-with\_\_\_\_\_underscore'
+ '@my-scope/my-package-with-regex-characters.+' | '@my-scope/my-package-with-regex-characters.+'
end
with_them do
@@ -74,7 +99,7 @@ RSpec.describe Packages::Protection::Rule, type: :model, feature_category: :pack
end
let_it_be(:ppr_with_wildcard_start) do
- create(:package_protection_rule, package_name_pattern: '*@my-scope/my_package-with-wildcard-start')
+ create(:package_protection_rule, package_name_pattern: '@my-scope/*my_package-with-wildcard-start')
end
let_it_be(:ppr_with_wildcard_end) do
@@ -82,11 +107,11 @@ RSpec.describe Packages::Protection::Rule, type: :model, feature_category: :pack
end
let_it_be(:ppr_with_wildcard_inbetween) do
- create(:package_protection_rule, package_name_pattern: '@my-scope/*my_package-with-wildcard-inbetween')
+ create(:package_protection_rule, package_name_pattern: '@my-scope/my_package*with-wildcard-inbetween')
end
let_it_be(:ppr_with_wildcard_multiples) do
- create(:package_protection_rule, package_name_pattern: '**@my-scope/**my_package-with-wildcard-multiple**')
+ create(:package_protection_rule, package_name_pattern: '@my-scope/**my_package**with-wildcard-multiple**')
end
let_it_be(:ppr_with_underscore) do
@@ -103,46 +128,47 @@ RSpec.describe Packages::Protection::Rule, type: :model, feature_category: :pack
context 'with several package protection rule scenarios' do
where(:package_name, :expected_package_protection_rules) do
- '@my-scope/my_package' | [ref(:package_protection_rule)]
- '@my-scope/my2package' | []
- '@my-scope/my_package-2' | []
+ '@my-scope/my_package' | [ref(:package_protection_rule)]
+ '@my-scope/my2package' | []
+ '@my-scope/my_package-2' | []
# With wildcard pattern at the start
- '@my-scope/my_package-with-wildcard-start' | [ref(:ppr_with_wildcard_start)]
- '@my-scope/my_package-with-wildcard-start-any' | []
- 'prefix-@my-scope/my_package-with-wildcard-start' | [ref(:ppr_with_wildcard_start)]
- 'prefix-@my-scope/my_package-with-wildcard-start-any' | []
+ '@my-scope/my_package-with-wildcard-start' | [ref(:ppr_with_wildcard_start)]
+ '@my-scope/my_package-with-wildcard-start-any' | []
+ '@my-scope/any-my_package-with-wildcard-start' | [ref(:ppr_with_wildcard_start)]
+ '@my-scope/any-my_package-with-wildcard-start-any' | []
# With wildcard pattern at the end
- '@my-scope/my_package-with-wildcard-end' | [ref(:ppr_with_wildcard_end)]
- '@my-scope/my_package-with-wildcard-end:1234567890' | [ref(:ppr_with_wildcard_end)]
- 'prefix-@my-scope/my_package-with-wildcard-end' | []
- 'prefix-@my-scope/my_package-with-wildcard-end:1234567890' | []
+ '@my-scope/my_package-with-wildcard-end' | [ref(:ppr_with_wildcard_end)]
+ '@my-scope/my_package-with-wildcard-end:1234567890' | [ref(:ppr_with_wildcard_end)]
+ '@my-scope/any-my_package-with-wildcard-end' | []
+ '@my-scope/any-my_package-with-wildcard-end:1234567890' | []
# With wildcard pattern inbetween
- '@my-scope/my_package-with-wildcard-inbetween' | [ref(:ppr_with_wildcard_inbetween)]
- '@my-scope/any-my_package-with-wildcard-inbetween' | [ref(:ppr_with_wildcard_inbetween)]
- '@my-scope/any-my_package-my_package-wildcard-inbetween-any' | []
+ '@my-scope/my_packagewith-wildcard-inbetween' | [ref(:ppr_with_wildcard_inbetween)]
+ '@my-scope/my_package-any-with-wildcard-inbetween' | [ref(:ppr_with_wildcard_inbetween)]
+ '@my-scope/any-my_package-my_package-wildcard-inbetween-any' | []
# With multiple wildcard pattern are used
- '@my-scope/my_package-with-wildcard-multiple' | [ref(:ppr_with_wildcard_multiples)]
- 'prefix-@my-scope/any-my_package-with-wildcard-multiple-any' | [ref(:ppr_with_wildcard_multiples)]
- '****@my-scope/****my_package-with-wildcard-multiple****' | [ref(:ppr_with_wildcard_multiples)]
- 'prefix-@other-scope/any-my_package-with-wildcard-multiple-any' | []
+ '@my-scope/my_packagewith-wildcard-multiple' | [ref(:ppr_with_wildcard_multiples)]
+ '@my-scope/any-my_package-any-with-wildcard-multiple-any' | [ref(:ppr_with_wildcard_multiples)]
+ '@my-scope/****my_package****with-wildcard-multiple****' | [ref(:ppr_with_wildcard_multiples)]
+ '@other-scope/any-my_package-with-wildcard-multiple-any' | []
# With underscore
- '@my-scope/my_package-with_____underscore' | [ref(:ppr_with_underscore)]
- '@my-scope/my_package-with_any_underscore' | []
+ '@my-scope/my_package-with_____underscore' | [ref(:ppr_with_underscore)]
+ '@my-scope/my_package-with_any_underscore' | []
- '@my-scope/my_package-with-regex-characters.+' | [ref(:ppr_with_regex_characters)]
- '@my-scope/my_package-with-regex-characters.' | []
- '@my-scope/my_package-with-regex-characters' | []
- '@my-scope/my_package-with-regex-characters-any' | []
+ # With regex pattern
+ '@my-scope/my_package-with-regex-characters.+' | [ref(:ppr_with_regex_characters)]
+ '@my-scope/my_package-with-regex-characters.' | []
+ '@my-scope/my_package-with-regex-characters' | []
+ '@my-scope/my_package-with-regex-characters-any' | []
# Special cases
- nil | []
- '' | []
- 'any_package' | []
+ nil | []
+ '' | []
+ 'any_package' | []
end
with_them do
@@ -209,7 +235,7 @@ RSpec.describe Packages::Protection::Rule, type: :model, feature_category: :pack
)
end
- describe "with different users and protection levels" do
+ describe 'with different users and protection levels' do
# rubocop:disable Layout/LineLength
where(:project, :access_level, :package_name, :package_type, :push_protected) do
ref(:project_with_ppr) | Gitlab::Access::REPORTER | '@my-scope/my-package-stage-sha-1234' | :npm | true
diff --git a/spec/models/packages/pypi/metadatum_spec.rb b/spec/models/packages/pypi/metadatum_spec.rb
index 6c83c4ed143..6c93f84124f 100644
--- a/spec/models/packages/pypi/metadatum_spec.rb
+++ b/spec/models/packages/pypi/metadatum_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-RSpec.describe Packages::Pypi::Metadatum, type: :model do
+RSpec.describe Packages::Pypi::Metadatum, type: :model, feature_category: :package_registry do
describe 'relationships' do
it { is_expected.to belong_to(:package) }
end
@@ -9,8 +9,29 @@ RSpec.describe Packages::Pypi::Metadatum, type: :model do
describe 'validations' do
it { is_expected.to validate_presence_of(:package) }
it { is_expected.to allow_value('').for(:required_python) }
- it { is_expected.not_to allow_value(nil).for(:required_python) }
- it { is_expected.not_to allow_value('a' * 256).for(:required_python) }
+ it { is_expected.to validate_length_of(:required_python).is_at_most(described_class::MAX_REQUIRED_PYTHON_LENGTH) }
+ it { is_expected.to allow_value('').for(:keywords) }
+ it { is_expected.to allow_value(nil).for(:keywords) }
+ it { is_expected.to validate_length_of(:keywords).is_at_most(described_class::MAX_KEYWORDS_LENGTH) }
+ it { is_expected.to allow_value('').for(:metadata_version) }
+ it { is_expected.to allow_value(nil).for(:metadata_version) }
+ it { is_expected.to validate_length_of(:metadata_version).is_at_most(described_class::MAX_METADATA_VERSION_LENGTH) }
+ it { is_expected.to allow_value('').for(:author_email) }
+ it { is_expected.to allow_value(nil).for(:author_email) }
+ it { is_expected.to validate_length_of(:author_email).is_at_most(described_class::MAX_AUTHOR_EMAIL_LENGTH) }
+ it { is_expected.to allow_value('').for(:summary) }
+ it { is_expected.to allow_value(nil).for(:summary) }
+ it { is_expected.to validate_length_of(:summary).is_at_most(described_class::MAX_SUMMARY_LENGTH) }
+ it { is_expected.to allow_value('').for(:description) }
+ it { is_expected.to allow_value(nil).for(:description) }
+ it { is_expected.to validate_length_of(:description).is_at_most(described_class::MAX_DESCRIPTION_LENGTH) }
+ it { is_expected.to allow_value('').for(:description_content_type) }
+ it { is_expected.to allow_value(nil).for(:description_content_type) }
+
+ it {
+ is_expected.to validate_length_of(:description_content_type)
+ .is_at_most(described_class::MAX_DESCRIPTION_CONTENT_TYPE)
+ }
describe '#pypi_package_type' do
it 'will not allow a package with a different package_type' do
diff --git a/spec/models/packages/tag_spec.rb b/spec/models/packages/tag_spec.rb
index bc03c34f56b..6842d1946e5 100644
--- a/spec/models/packages/tag_spec.rb
+++ b/spec/models/packages/tag_spec.rb
@@ -5,6 +5,23 @@ RSpec.describe Packages::Tag, type: :model, feature_category: :package_registry
let!(:project) { create(:project) }
let!(:package) { create(:npm_package, version: '1.0.2', project: project, updated_at: 3.days.ago) }
+ describe '#ensure_project_id' do
+ it 'sets the project_id before saving' do
+ tag = build(:packages_tag)
+ expect(tag.project_id).to be_nil
+ tag.save!
+ expect(tag.project_id).not_to be_nil
+ expect(tag.project_id).to eq(tag.package.project_id)
+ end
+
+ it 'does not override the project_id if set' do
+ another_project = create(:project)
+ tag = build(:packages_tag, project_id: another_project.id)
+ tag.save!
+ expect(tag.project_id).to eq(another_project.id)
+ end
+ end
+
describe 'relationships' do
it { is_expected.to belong_to(:package).inverse_of(:tags) }
end
@@ -61,7 +78,7 @@ RSpec.describe Packages::Tag, type: :model, feature_category: :package_registry
end
context 'with multiple names' do
- let(:name) { %w(tag1 tag3) }
+ let(:name) { %w[tag1 tag3] }
it { is_expected.to contain_exactly(tag1, tag3) }
end
diff --git a/spec/models/pages/lookup_path_spec.rb b/spec/models/pages/lookup_path_spec.rb
index 08ba823f8fa..570c369016b 100644
--- a/spec/models/pages/lookup_path_spec.rb
+++ b/spec/models/pages/lookup_path_spec.rb
@@ -55,12 +55,7 @@ RSpec.describe Pages::LookupPath, feature_category: :pages do
end
context 'when there is pages deployment' do
- let(:deployment) { create(:pages_deployment, project: project) }
-
- before do
- project.mark_pages_as_deployed
- project.pages_metadatum.update!(pages_deployment: deployment)
- end
+ let!(:deployment) { create(:pages_deployment, project: project) }
it 'uses deployment from object storage' do
freeze_time do
@@ -75,12 +70,6 @@ RSpec.describe Pages::LookupPath, feature_category: :pages do
end
end
- it 'does not recreate source hash' do
- expect(deployment.file).to receive(:url_or_file_path).once
-
- 2.times { lookup_path.source }
- end
-
context 'when deployment is in the local storage' do
before do
deployment.file.migrate!(::ObjectStorage::Store::LOCAL)
@@ -159,12 +148,7 @@ RSpec.describe Pages::LookupPath, feature_category: :pages do
end
context 'when there is a deployment' do
- let(:deployment) { create(:pages_deployment, project: project, root_directory: 'foo') }
-
- before do
- project.mark_pages_as_deployed
- project.pages_metadatum.update!(pages_deployment: deployment)
- end
+ let!(:deployment) { create(:pages_deployment, project: project, root_directory: 'foo') }
it 'returns the deployment\'s root_directory' do
expect(lookup_path.root_directory).to eq('foo')
diff --git a/spec/models/pages_deployment_spec.rb b/spec/models/pages_deployment_spec.rb
index e74c7ee8612..1e6f8b33a86 100644
--- a/spec/models/pages_deployment_spec.rb
+++ b/spec/models/pages_deployment_spec.rb
@@ -71,54 +71,62 @@ RSpec.describe PagesDeployment, feature_category: :pages do
end
end
- describe '.deactivate_deployments_older_than', :freeze_time do
- let!(:other_project_deployment) do
- create(:pages_deployment)
- end
+ describe '.deactivate_all', :freeze_time do
+ let!(:deployment) { create(:pages_deployment, project: project, updated_at: 5.minutes.ago) }
+ let!(:nil_path_prefix_deployment) { create(:pages_deployment, project: project, path_prefix: nil) }
+ let!(:empty_path_prefix_deployment) { create(:pages_deployment, project: project, path_prefix: '') }
- let!(:other_path_prefix_deployment) do
- create(:pages_deployment, project: project, path_prefix: 'other')
- end
+ let!(:other_project_deployment) { create(:pages_deployment) }
+ let!(:deactivated_deployment) { create(:pages_deployment, project: project, deleted_at: 5.minutes.ago) }
- let!(:deactivated_deployment) do
- create(:pages_deployment, project: project, deleted_at: 5.minutes.ago)
+ it 'updates only older deployments for the same project and path prefix' do
+ expect { described_class.deactivate_all(project) }
+ .to change { deployment.reload.deleted_at }.from(nil).to(Time.zone.now)
+ .and change { deployment.reload.updated_at }.to(Time.zone.now)
+ .and change { nil_path_prefix_deployment.reload.deleted_at }.from(nil).to(Time.zone.now)
+ .and change { empty_path_prefix_deployment.reload.deleted_at }.from(nil).to(Time.zone.now)
+ .and not_change { other_project_deployment.reload.deleted_at }
+ .and not_change { deactivated_deployment.reload.deleted_at }
end
+ end
+
+ describe '.deactivate_deployments_older_than', :freeze_time do
+ let!(:nil_path_prefix_deployment) { create(:pages_deployment, project: project, path_prefix: nil) }
+ let!(:empty_path_prefix_deployment) { create(:pages_deployment, project: project, path_prefix: '') }
+ let!(:older_deployment) { create(:pages_deployment, project: project, updated_at: 5.minutes.ago) }
+ let!(:reference_deployment) { create(:pages_deployment, project: project, updated_at: 5.minutes.ago) }
+ let!(:newer_deployment) { create(:pages_deployment, project: project, updated_at: 5.minutes.ago) }
+
+ let!(:other_project_deployment) { create(:pages_deployment) }
+ let!(:other_path_prefix_deployment) { create(:pages_deployment, project: project, path_prefix: 'other') }
+ let!(:deactivated_deployment) { create(:pages_deployment, project: project, deleted_at: 5.minutes.ago) }
it 'updates only older deployments for the same project and path prefix' do
- deployment1 = create(:pages_deployment, project: project, updated_at: 5.minutes.ago)
- deployment2 = create(:pages_deployment, project: project, updated_at: 5.minutes.ago)
- deployment3 = create(:pages_deployment, project: project, updated_at: 5.minutes.ago)
-
- expect { described_class.deactivate_deployments_older_than(deployment2) }
- .to change { deployment1.reload.deleted_at }
- .from(nil).to(Time.zone.now)
- .and change { deployment1.reload.updated_at }
- .to(Time.zone.now)
-
- expect(deployment2.reload.deleted_at).to be_nil
- expect(deployment3.reload.deleted_at).to be_nil
- expect(other_project_deployment.deleted_at).to be_nil
- expect(other_path_prefix_deployment.reload.deleted_at).to be_nil
- expect(deactivated_deployment.reload.deleted_at).to eq(5.minutes.ago)
+ expect { described_class.deactivate_deployments_older_than(reference_deployment) }
+ .to change { older_deployment.reload.deleted_at }.from(nil).to(Time.zone.now)
+ .and change { older_deployment.reload.updated_at }.to(Time.zone.now)
+ .and change { nil_path_prefix_deployment.reload.deleted_at }.from(nil).to(Time.zone.now)
+ .and change { empty_path_prefix_deployment.reload.deleted_at }.from(nil).to(Time.zone.now)
+ .and not_change { reference_deployment.reload.deleted_at }
+ .and not_change { newer_deployment.reload.deleted_at }
+ .and not_change { other_project_deployment.reload.deleted_at }
+ .and not_change { other_path_prefix_deployment.reload.deleted_at }
+ .and not_change { deactivated_deployment.reload.deleted_at }
end
it 'updates only older deployments for the same project with the given time' do
- deployment1 = create(:pages_deployment, project: project, updated_at: 5.minutes.ago)
- deployment2 = create(:pages_deployment, project: project, updated_at: 5.minutes.ago)
- deployment3 = create(:pages_deployment, project: project, updated_at: 5.minutes.ago)
time = 30.minutes.from_now
- expect { described_class.deactivate_deployments_older_than(deployment2, time: time) }
- .to change { deployment1.reload.deleted_at }
- .from(nil).to(time)
- .and change { deployment1.reload.updated_at }
- .to(Time.zone.now)
-
- expect(deployment2.reload.deleted_at).to be_nil
- expect(deployment3.reload.deleted_at).to be_nil
- expect(other_project_deployment.deleted_at).to be_nil
- expect(other_path_prefix_deployment.reload.deleted_at).to be_nil
- expect(deactivated_deployment.reload.deleted_at).to eq(5.minutes.ago)
+ expect { described_class.deactivate_deployments_older_than(reference_deployment, time: time) }
+ .to change { older_deployment.reload.deleted_at }.from(nil).to(time)
+ .and change { older_deployment.reload.updated_at }.to(Time.zone.now)
+ .and change { nil_path_prefix_deployment.reload.deleted_at }.from(nil).to(time)
+ .and change { empty_path_prefix_deployment.reload.deleted_at }.from(nil).to(time)
+ .and not_change { reference_deployment.reload.deleted_at }
+ .and not_change { newer_deployment.reload.deleted_at }
+ .and not_change { other_project_deployment.reload.deleted_at }
+ .and not_change { other_path_prefix_deployment.reload.deleted_at }
+ .and not_change { deactivated_deployment.reload.deleted_at }
end
end
diff --git a/spec/models/pages_domain_spec.rb b/spec/models/pages_domain_spec.rb
index 5a4eca11f71..7aa5cf993dc 100644
--- a/spec/models/pages_domain_spec.rb
+++ b/spec/models/pages_domain_spec.rb
@@ -84,20 +84,20 @@ RSpec.describe PagesDomain, feature_category: :pages do
attributes = attributes_for(:pages_domain)
cert, key = attributes.fetch_values(:certificate, :key)
- true | nil | nil | false | %i(certificate key)
+ true | nil | nil | false | %i[certificate key]
true | nil | nil | true | []
- true | cert | nil | false | %i(key)
- true | cert | nil | true | %i(key)
- true | nil | key | false | %i(certificate key)
- true | nil | key | true | %i(key)
+ true | cert | nil | false | %i[key]
+ true | cert | nil | true | %i[key]
+ true | nil | key | false | %i[certificate key]
+ true | nil | key | true | %i[key]
true | cert | key | false | []
true | cert | key | true | []
false | nil | nil | false | []
false | nil | nil | true | []
- false | cert | nil | false | %i(key)
- false | cert | nil | true | %i(key)
- false | nil | key | false | %i(key)
- false | nil | key | true | %i(key)
+ false | cert | nil | false | %i[key]
+ false | cert | nil | true | %i[key]
+ false | nil | key | false | %i[key]
+ false | nil | key | true | %i[key]
false | cert | key | false | []
false | cert | key | true | []
end
@@ -288,8 +288,8 @@ RSpec.describe PagesDomain, feature_category: :pages do
end
end
- describe '#has_intermediates?' do
- subject { domain.has_intermediates? }
+ describe '#has_valid_intermediates?' do
+ subject { domain.has_valid_intermediates? }
context 'for self signed' do
let(:domain) { build(:pages_domain) }
@@ -312,6 +312,14 @@ RSpec.describe PagesDomain, feature_category: :pages do
it { is_expected.to be_truthy }
end
+
+ context 'for chain with unknown root CA' do
+ # In cases where users use an origin certificate the CA does not necessarily need to be in
+ # the trust store, eg. in the case of Cloudflare Origin Certs.
+ let(:domain) { build(:pages_domain, :with_untrusted_root_ca_in_chain) }
+
+ it { is_expected.to be_truthy }
+ end
end
describe '#expired?' do
diff --git a/spec/models/personal_access_token_spec.rb b/spec/models/personal_access_token_spec.rb
index 7437e9b463e..7665f4dbde4 100644
--- a/spec/models/personal_access_token_spec.rb
+++ b/spec/models/personal_access_token_spec.rb
@@ -365,7 +365,7 @@ RSpec.describe PersonalAccessToken, feature_category: :system_access do
describe '.simple_sorts' do
it 'includes overridden keys' do
- expect(described_class.simple_sorts.keys).to include(*%w(expires_at_asc_id_desc))
+ expect(described_class.simple_sorts.keys).to include(*%w[expires_at_asc_id_desc])
end
end
diff --git a/spec/models/project_feature_spec.rb b/spec/models/project_feature_spec.rb
index 39e77df1900..c0a78ff2f53 100644
--- a/spec/models/project_feature_spec.rb
+++ b/spec/models/project_feature_spec.rb
@@ -53,7 +53,7 @@ RSpec.describe ProjectFeature, feature_category: :groups_and_projects do
end
it "does not allow repository related features have higher level" do
- features = %w(builds merge_requests)
+ features = %w[builds merge_requests]
project_feature = project.project_feature
features.each do |feature|
@@ -64,7 +64,7 @@ RSpec.describe ProjectFeature, feature_category: :groups_and_projects do
end
end
- it_behaves_like 'access level validation', ProjectFeature::FEATURES - %i(pages package_registry) do
+ it_behaves_like 'access level validation', ProjectFeature::FEATURES - %i[pages package_registry] do
let(:container_features) { project.project_feature }
end
diff --git a/spec/models/project_feature_usage_spec.rb b/spec/models/project_feature_usage_spec.rb
deleted file mode 100644
index 3765a2b37a7..00000000000
--- a/spec/models/project_feature_usage_spec.rb
+++ /dev/null
@@ -1,173 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe ProjectFeatureUsage, type: :model do
- describe '.jira_dvcs_integrations_enabled_count' do
- it 'returns count of projects with Jira DVCS Cloud enabled' do
- create(:project).feature_usage.log_jira_dvcs_integration_usage
- create(:project).feature_usage.log_jira_dvcs_integration_usage
-
- expect(described_class.with_jira_dvcs_integration_enabled.count).to eq(2)
- end
-
- it 'returns count of projects with Jira DVCS Server enabled' do
- create(:project).feature_usage.log_jira_dvcs_integration_usage(cloud: false)
- create(:project).feature_usage.log_jira_dvcs_integration_usage(cloud: false)
-
- expect(described_class.with_jira_dvcs_integration_enabled(cloud: false).count).to eq(2)
- end
- end
-
- describe '#log_jira_dvcs_integration_usage' do
- let(:project) { create(:project) }
-
- subject { project.feature_usage }
-
- context 'when the feature usage has not been created yet' do
- it 'logs Jira DVCS Cloud last sync' do
- freeze_time do
- subject.log_jira_dvcs_integration_usage
-
- expect(subject.jira_dvcs_server_last_sync_at).to be_nil
- expect(subject.jira_dvcs_cloud_last_sync_at).to be_like_time(Time.current)
- end
- end
-
- it 'logs Jira DVCS Server last sync' do
- freeze_time do
- subject.log_jira_dvcs_integration_usage(cloud: false)
-
- expect(subject.jira_dvcs_server_last_sync_at).to be_like_time(Time.current)
- expect(subject.jira_dvcs_cloud_last_sync_at).to be_nil
- end
- end
- end
-
- context 'when the feature usage already exists' do
- let(:today) { Time.current.beginning_of_day }
- let(:project) { create(:project) }
-
- subject { project.feature_usage }
-
- where(:cloud, :timestamp_field) do
- [
- [true, :jira_dvcs_cloud_last_sync_at],
- [false, :jira_dvcs_server_last_sync_at]
- ]
- end
-
- with_them do
- context 'when Jira DVCS Cloud last sync has not been logged' do
- before do
- travel_to today - 3.days do
- subject.log_jira_dvcs_integration_usage(cloud: !cloud)
- end
- end
-
- it 'logs Jira DVCS Cloud last sync' do
- freeze_time do
- subject.log_jira_dvcs_integration_usage(cloud: cloud)
-
- expect(subject.reload.send(timestamp_field)).to be_like_time(Time.current)
- end
- end
- end
-
- context 'when Jira DVCS Cloud last sync was logged today' do
- let(:last_updated) { today + 1.hour }
-
- before do
- travel_to last_updated do
- subject.log_jira_dvcs_integration_usage(cloud: cloud)
- end
- end
-
- it 'does not log Jira DVCS Cloud last sync' do
- travel_to today + 2.hours do
- subject.log_jira_dvcs_integration_usage(cloud: cloud)
-
- expect(subject.reload.send(timestamp_field)).to be_like_time(last_updated)
- end
- end
- end
-
- context 'when Jira DVCS Cloud last sync was logged yesterday' do
- let(:last_updated) { today - 2.days }
-
- before do
- travel_to last_updated do
- subject.log_jira_dvcs_integration_usage(cloud: cloud)
- end
- end
-
- it 'logs Jira DVCS Cloud last sync' do
- travel_to today + 1.hour do
- subject.log_jira_dvcs_integration_usage(cloud: cloud)
-
- expect(subject.reload.send(timestamp_field)).to be_like_time(today + 1.hour)
- end
- end
- end
- end
- end
-
- context 'when log_jira_dvcs_integration_usage is called simultaneously for the same project' do
- it 'logs the latest call' do
- feature_usage = project.feature_usage
- feature_usage.log_jira_dvcs_integration_usage
- first_logged_at = feature_usage.jira_dvcs_cloud_last_sync_at
-
- travel_to(1.hour.from_now) do
- ProjectFeatureUsage.new(project_id: project.id).log_jira_dvcs_integration_usage
- end
-
- expect(feature_usage.reload.jira_dvcs_cloud_last_sync_at).to be > first_logged_at
- end
- end
- end
-
- context 'ProjectFeatureUsage with DB Load Balancing', :request_store do
- describe '#log_jira_dvcs_integration_usage' do
- let!(:project) { create(:project) }
-
- subject { project.feature_usage }
-
- context 'database load balancing is configured' do
- before do
- ::Gitlab::Database::LoadBalancing::Session.clear_session
- end
-
- it 'logs Jira DVCS Cloud last sync' do
- freeze_time do
- subject.log_jira_dvcs_integration_usage
-
- expect(subject.jira_dvcs_server_last_sync_at).to be_nil
- expect(subject.jira_dvcs_cloud_last_sync_at).to be_like_time(Time.current)
- end
- end
-
- it 'does not stick to primary' do
- expect(::Gitlab::Database::LoadBalancing::Session.current).not_to be_performed_write
- expect(::Gitlab::Database::LoadBalancing::Session.current).not_to be_using_primary
-
- subject.log_jira_dvcs_integration_usage
-
- expect(::Gitlab::Database::LoadBalancing::Session.current).to be_performed_write
- expect(::Gitlab::Database::LoadBalancing::Session.current).not_to be_using_primary
- end
- end
-
- context 'database load balancing is not cofigured' do
- it 'logs Jira DVCS Cloud last sync' do
- freeze_time do
- subject.log_jira_dvcs_integration_usage
-
- expect(subject.jira_dvcs_server_last_sync_at).to be_nil
- expect(subject.jira_dvcs_cloud_last_sync_at).to be_like_time(Time.current)
- end
- end
- end
- end
- end
-end
diff --git a/spec/models/project_label_spec.rb b/spec/models/project_label_spec.rb
index 62839f5fb4f..01df58ee615 100644
--- a/spec/models/project_label_spec.rb
+++ b/spec/models/project_label_spec.rb
@@ -89,7 +89,7 @@ RSpec.describe ProjectLabel do
end
it 'uses id when name contains double quote' do
- label = create(:label, name: %q{"irony"})
+ label = create(:label, name: %q("irony"))
expect(label.to_reference(format: :name)).to eq "~#{label.id}"
end
end
diff --git a/spec/models/project_setting_spec.rb b/spec/models/project_setting_spec.rb
index 719e51018ac..8ad232b7e0c 100644
--- a/spec/models/project_setting_spec.rb
+++ b/spec/models/project_setting_spec.rb
@@ -214,7 +214,7 @@ RSpec.describe ProjectSetting, type: :model, feature_category: :groups_and_proje
context 'when emails are enabled in parent group' do
before do
- allow(project.namespace).to receive(:emails_disabled?).and_return(false)
+ allow(project.namespace).to receive(:emails_enabled?).and_return(true)
end
it 'returns true' do
diff --git a/spec/models/project_snippet_spec.rb b/spec/models/project_snippet_spec.rb
index 3d1c87771f3..5ce9499c420 100644
--- a/spec/models/project_snippet_spec.rb
+++ b/spec/models/project_snippet_spec.rb
@@ -2,11 +2,24 @@
require 'spec_helper'
-RSpec.describe ProjectSnippet do
+RSpec.describe ProjectSnippet, feature_category: :source_code_management do
describe "Associations" do
it { is_expected.to belong_to(:project) }
end
+ describe 'scopes' do
+ describe '.by_project' do
+ subject { described_class.by_project(project) }
+
+ let_it_be(:project) { create(:project) }
+ let_it_be(:snippet1) { create(:project_snippet, project: project) }
+ let_it_be(:snippet2) { create(:project_snippet, project: build(:project)) }
+ let_it_be(:snippet3) { create(:personal_snippet) }
+
+ it { is_expected.to contain_exactly(snippet1) }
+ end
+ end
+
describe "Validation" do
it { is_expected.to validate_presence_of(:project) }
it { is_expected.to validate_inclusion_of(:secret).in_array([false]) }
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index c27ed2cc82c..3ea5f6ea0ae 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -1136,24 +1136,24 @@ RSpec.describe Project, factory_default: :keep, feature_category: :groups_and_pr
it { is_expected.to delegate_method(:npm_package_requests_forwarding).to(:namespace) }
describe 'read project settings' do
- %i(
+ %i[
show_default_award_emojis
show_default_award_emojis?
warn_about_potentially_unwanted_characters
warn_about_potentially_unwanted_characters?
enforce_auth_checks_on_uploads
enforce_auth_checks_on_uploads?
- ).each do |method|
+ ].each do |method|
it { is_expected.to delegate_method(method).to(:project_setting).allow_nil }
end
end
describe 'write project settings' do
- %i(
+ %i[
show_default_award_emojis=
warn_about_potentially_unwanted_characters=
enforce_auth_checks_on_uploads=
- ).each do |method|
+ ].each do |method|
it { is_expected.to delegate_method(method).to(:project_setting).with_arguments(:args).allow_nil }
end
end
@@ -1177,12 +1177,13 @@ RSpec.describe Project, factory_default: :keep, feature_category: :groups_and_pr
let(:exclude_attributes) do
# Skip attributes defined in EE code
- %w(
+ %w[
merge_pipelines_enabled
merge_trains_enabled
auto_rollback_enabled
merge_trains_skip_train_allowed
- )
+ restrict_pipeline_cancellation_role
+ ]
end
end
@@ -2127,28 +2128,6 @@ RSpec.describe Project, factory_default: :keep, feature_category: :groups_and_pr
end
end
- describe 'sorting by name' do
- let_it_be(:project1) { create(:project, name: 'A') }
- let_it_be(:project2) { create(:project, name: 'Z') }
- let_it_be(:project3) { create(:project, name: 'L') }
-
- context 'when using .sort_by_name_desc' do
- it 'reorders the projects by descending name order' do
- projects = described_class.sorted_by_name_desc
-
- expect(projects.pluck(:name)).to eq(%w[Z L A])
- end
- end
-
- context 'when using .sort_by_name_asc' do
- it 'reorders the projects by ascending name order' do
- projects = described_class.sorted_by_name_asc
-
- expect(projects.pluck(:name)).to eq(%w[A L Z])
- end
- end
- end
-
describe '.with_shared_runners_enabled' do
subject { described_class.with_shared_runners_enabled }
@@ -2841,7 +2820,7 @@ RSpec.describe Project, factory_default: :keep, feature_category: :groups_and_pr
context "will return false if pages is deployed even if onboarding_complete is false" do
before do
project.pages_metadatum.update_column(:onboarding_complete, false)
- project.pages_metadatum.update_column(:deployed, true)
+ create(:pages_deployment, project: project)
end
it { is_expected.to be_falsey }
@@ -2855,7 +2834,7 @@ RSpec.describe Project, factory_default: :keep, feature_category: :groups_and_pr
context 'if pages are deployed' do
before do
- project.pages_metadatum.update_column(:deployed, true)
+ create(:pages_deployment, project: project)
end
it { is_expected.to be_truthy }
@@ -4309,7 +4288,7 @@ RSpec.describe Project, factory_default: :keep, feature_category: :groups_and_pr
end
context 'when project has a deployment platform' do
- let(:platform_variables) { %w(platform variables) }
+ let(:platform_variables) { %w[platform variables] }
let(:deployment_platform) { double }
before do
@@ -7142,7 +7121,7 @@ RSpec.describe Project, factory_default: :keep, feature_category: :groups_and_pr
project.check_personal_projects_limit
expect(project.errors[:limit_reached].first)
- .to match(/Personal project creation is not allowed/)
+ .to eq('You cannot create projects in your personal namespace. Contact your GitLab administrator.')
end
end
@@ -7155,7 +7134,7 @@ RSpec.describe Project, factory_default: :keep, feature_category: :groups_and_pr
project.check_personal_projects_limit
expect(project.errors[:limit_reached].first)
- .to match(/Your project limit is 5 projects/)
+ .to eq("You've reached your limit of 5 projects created. Contact your GitLab administrator.")
end
end
end
@@ -7219,126 +7198,6 @@ RSpec.describe Project, factory_default: :keep, feature_category: :groups_and_pr
end
end
- describe '#mark_pages_as_deployed' do
- let(:project) { create(:project) }
-
- it "works when artifacts_archive is missing" do
- project.mark_pages_as_deployed
-
- expect(project.pages_metadatum.reload.deployed).to eq(true)
- end
-
- it "creates new record and sets deployed to true if none exists yet" do
- project.pages_metadatum.destroy!
- project.reload
-
- project.mark_pages_as_deployed
-
- expect(project.pages_metadatum.reload.deployed).to eq(true)
- end
-
- it "updates the existing record and sets deployed to true and records artifact archive" do
- pages_metadatum = project.pages_metadatum
- pages_metadatum.update!(deployed: false)
-
- expect do
- project.mark_pages_as_deployed
- end.to change { pages_metadatum.reload.deployed }.from(false).to(true)
- end
- end
-
- describe '#mark_pages_as_not_deployed' do
- let(:project) { create(:project) }
-
- it "creates new record and sets deployed to false if none exists yet" do
- project.pages_metadatum.destroy!
- project.reload
-
- project.mark_pages_as_not_deployed
-
- expect(project.pages_metadatum.reload.deployed).to eq(false)
- end
-
- it "updates the existing record and sets deployed to false and clears artifacts_archive" do
- pages_metadatum = project.pages_metadatum
- pages_metadatum.update!(deployed: true)
-
- expect do
- project.mark_pages_as_not_deployed
- end.to change { pages_metadatum.reload.deployed }.from(true).to(false)
- end
- end
-
- describe '#update_pages_deployment!' do
- let(:project) { create(:project) }
- let(:deployment) { create(:pages_deployment, project: project) }
-
- it "creates new metadata record if none exists yet and sets deployment" do
- project.pages_metadatum.destroy!
- project.reload
-
- project.update_pages_deployment!(deployment)
-
- expect(project.pages_metadatum.pages_deployment).to eq(deployment)
- end
-
- it "updates the existing metadara record with deployment" do
- expect do
- project.update_pages_deployment!(deployment)
- end.to change { project.pages_metadatum.reload.pages_deployment }.from(nil).to(deployment)
- end
- end
-
- describe '#set_first_pages_deployment!' do
- let(:project) { create(:project) }
- let(:deployment) { create(:pages_deployment, project: project) }
-
- it "creates new metadata record if none exists yet and sets deployment" do
- project.pages_metadatum.destroy!
- project.reload
-
- project.set_first_pages_deployment!(deployment)
-
- expect(project.pages_metadatum.reload.pages_deployment).to eq(deployment)
- expect(project.pages_metadatum.reload.deployed).to eq(true)
- end
-
- it "updates the existing metadara record with deployment" do
- expect do
- project.set_first_pages_deployment!(deployment)
- end.to change { project.pages_metadatum.reload.pages_deployment }.from(nil).to(deployment)
-
- expect(project.pages_metadatum.reload.deployed).to eq(true)
- end
-
- it 'only updates metadata for this project' do
- other_project = create(:project)
-
- expect do
- project.set_first_pages_deployment!(deployment)
- end.not_to change { other_project.pages_metadatum.reload.pages_deployment }.from(nil)
-
- expect(other_project.pages_metadatum.reload.deployed).to eq(false)
- end
-
- it 'does nothing if metadata already references some deployment' do
- existing_deployment = create(:pages_deployment, project: project)
- project.set_first_pages_deployment!(existing_deployment)
-
- expect do
- project.set_first_pages_deployment!(deployment)
- end.not_to change { project.pages_metadatum.reload.pages_deployment }.from(existing_deployment)
- end
-
- it 'marks project as not deployed if deployment is nil' do
- project.mark_pages_as_deployed
-
- expect do
- project.set_first_pages_deployment!(nil)
- end.to change { project.pages_metadatum.reload.deployed }.from(true).to(false)
- end
- end
-
describe '#has_pool_repository?' do
it 'returns false when it does not have a pool repository' do
subject = create(:project, :repository)
@@ -7402,7 +7261,7 @@ RSpec.describe Project, factory_default: :keep, feature_category: :groups_and_pr
it 'returns only projects that have pages deployed' do
_project_without_pages = create(:project)
project_with_pages = create(:project)
- project_with_pages.mark_pages_as_deployed
+ create(:pages_deployment, project: project_with_pages)
expect(described_class.with_pages_deployed).to contain_exactly(project_with_pages)
end
@@ -9141,6 +9000,66 @@ RSpec.describe Project, factory_default: :keep, feature_category: :groups_and_pr
end
end
+ # TODO: Remove/update this spec after background syncing is implemented. See https://gitlab.com/gitlab-org/gitlab/-/issues/429376.
+ describe '#update_catalog_resource' do
+ let_it_be_with_reload(:project) { create(:project, name: 'My project name', description: 'My description') }
+ let_it_be_with_reload(:resource) { create(:ci_catalog_resource, project: project) }
+
+ shared_examples 'name, description, and visibility_level of the catalog resource match the project' do
+ it do
+ expect(project).to receive(:update_catalog_resource).once.and_call_original
+
+ project.save!
+
+ expect(resource.name).to eq(project.name)
+ expect(resource.description).to eq(project.description)
+ expect(resource.visibility_level).to eq(project.visibility_level)
+ end
+ end
+
+ context 'when the project name is updated' do
+ before do
+ project.name = 'My new project name'
+ end
+
+ it_behaves_like 'name, description, and visibility_level of the catalog resource match the project'
+ end
+
+ context 'when the project description is updated' do
+ before do
+ project.description = 'My new description'
+ end
+
+ it_behaves_like 'name, description, and visibility_level of the catalog resource match the project'
+ end
+
+ context 'when the project visibility_level is updated' do
+ before do
+ project.visibility_level = 10
+ end
+
+ it_behaves_like 'name, description, and visibility_level of the catalog resource match the project'
+ end
+
+ context 'when neither the project name, description, nor visibility_level are updated' do
+ it 'does not call update_catalog_resource' do
+ expect(project).not_to receive(:update_catalog_resource)
+
+ project.update!(path: 'path')
+ end
+ end
+
+ context 'when the project does not have a catalog resource' do
+ let_it_be(:project2) { create(:project) }
+
+ it 'does not call update_catalog_resource' do
+ expect(project2).not_to receive(:update_catalog_resource)
+
+ project.update!(name: 'name')
+ end
+ end
+ end
+
private
def finish_job(export_job)
diff --git a/spec/models/projects/repository_storage_move_spec.rb b/spec/models/projects/repository_storage_move_spec.rb
index ab0ad81f77a..c5fbc92176f 100644
--- a/spec/models/projects/repository_storage_move_spec.rb
+++ b/spec/models/projects/repository_storage_move_spec.rb
@@ -3,33 +3,11 @@
require 'spec_helper'
RSpec.describe Projects::RepositoryStorageMove, type: :model do
- let_it_be_with_refind(:project) { create(:project) }
-
it_behaves_like 'handles repository moves' do
- let(:container) { project }
+ let_it_be_with_refind(:container) { create(:project) }
+
let(:repository_storage_factory_key) { :project_repository_storage_move }
let(:error_key) { :project }
let(:repository_storage_worker) { Projects::UpdateRepositoryStorageWorker }
end
-
- describe 'state transitions' do
- let(:storage) { 'test_second_storage' }
-
- before do
- stub_storage_settings(storage => { 'path' => 'tmp/tests/extra_storage' })
- end
-
- context 'when started' do
- subject(:storage_move) { create(:project_repository_storage_move, :started, container: project, destination_storage_name: storage) }
-
- context 'and transits to replicated' do
- it 'sets the repository storage and marks the container as writable' do
- storage_move.finish_replication!
-
- expect(project.repository_storage).to eq(storage)
- expect(project).not_to be_repository_read_only
- end
- end
- end
- end
end
diff --git a/spec/models/projects/topic_spec.rb b/spec/models/projects/topic_spec.rb
index 568a4166de7..b3a55ccd370 100644
--- a/spec/models/projects/topic_spec.rb
+++ b/spec/models/projects/topic_spec.rb
@@ -76,7 +76,7 @@ RSpec.describe Projects::Topic do
describe '#find_by_name_case_insensitive' do
it 'returns topic with case insensitive name' do
- %w(topic TOPIC Topic).each do |name|
+ %w[topic TOPIC Topic].each do |name|
expect(described_class.find_by_name_case_insensitive(name)).to eq(topic)
end
end
diff --git a/spec/models/prometheus_metric_spec.rb b/spec/models/prometheus_metric_spec.rb
index a20f4edcf4a..c8a95aef8a6 100644
--- a/spec/models/prometheus_metric_spec.rb
+++ b/spec/models/prometheus_metric_spec.rb
@@ -94,16 +94,16 @@ RSpec.describe PrometheusMetric do
describe '#required_metrics' do
where(:group, :required_metrics) do
- :nginx_ingress_vts | %w(nginx_upstream_responses_total nginx_upstream_response_msecs_avg)
- :nginx_ingress | %w(nginx_ingress_controller_requests nginx_ingress_controller_ingress_upstream_latency_seconds_sum)
- :ha_proxy | %w(haproxy_frontend_http_requests_total haproxy_frontend_http_responses_total)
- :aws_elb | %w(aws_elb_request_count_sum aws_elb_latency_average aws_elb_httpcode_backend_5_xx_sum)
- :nginx | %w(nginx_server_requests nginx_server_requestMsec)
- :kubernetes | %w(container_memory_usage_bytes container_cpu_usage_seconds_total)
- :business | %w()
- :response | %w()
- :system | %w()
- :cluster_health | %w(container_memory_usage_bytes container_cpu_usage_seconds_total)
+ :nginx_ingress_vts | %w[nginx_upstream_responses_total nginx_upstream_response_msecs_avg]
+ :nginx_ingress | %w[nginx_ingress_controller_requests nginx_ingress_controller_ingress_upstream_latency_seconds_sum]
+ :ha_proxy | %w[haproxy_frontend_http_requests_total haproxy_frontend_http_responses_total]
+ :aws_elb | %w[aws_elb_request_count_sum aws_elb_latency_average aws_elb_httpcode_backend_5_xx_sum]
+ :nginx | %w[nginx_server_requests nginx_server_requestMsec]
+ :kubernetes | %w[container_memory_usage_bytes container_cpu_usage_seconds_total]
+ :business | %w[]
+ :response | %w[]
+ :system | %w[]
+ :cluster_health | %w[container_memory_usage_bytes container_cpu_usage_seconds_total]
end
with_them do
diff --git a/spec/models/releases/link_spec.rb b/spec/models/releases/link_spec.rb
index c4c9fba32d9..5d264af695b 100644
--- a/spec/models/releases/link_spec.rb
+++ b/spec/models/releases/link_spec.rb
@@ -82,7 +82,7 @@ RSpec.describe Releases::Link do
describe 'supported protocols' do
where(:protocol) do
- %w(http https ftp)
+ %w[http https ftp]
end
with_them do
diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb
index 2265d1b39af..606c4ea05b9 100644
--- a/spec/models/repository_spec.rb
+++ b/spec/models/repository_spec.rb
@@ -743,7 +743,7 @@ RSpec.describe Repository, feature_category: :source_code_management do
describe "#merged_branch_names", :clean_gitlab_redis_cache do
subject { repository.merged_branch_names(branch_names) }
- let(:branch_names) { %w(test beep boop definitely_merged) }
+ let(:branch_names) { %w[test beep boop definitely_merged] }
let(:already_merged) { Set.new(["definitely_merged"]) }
let(:write_hash) do
@@ -1621,16 +1621,16 @@ RSpec.describe Repository, feature_category: :source_code_management do
where(:branch_names, :tag_names, :result) do
nil | nil | false
- %w() | %w() | false
- %w(a b) | %w() | false
- %w() | %w(c d) | false
- %w(a b) | %w(c d) | false
- %w(a/b) | %w(c/d) | false
- %w(a b) | %w(c d a/z) | true
- %w(a b c/z) | %w(c d) | true
- %w(a/b/z) | %w(a/b) | false # we only consider refs ambiguous before the first slash
- %w(a/b/z) | %w(a/b a) | true
- %w(ab) | %w(abc/d a b) | false
+ %w[] | %w[] | false
+ %w[a b] | %w[] | false
+ %w[] | %w[c d] | false
+ %w[a b] | %w[c d] | false
+ %w[a/b] | %w[c/d] | false
+ %w[a b] | %w[c d a/z] | true
+ %w[a b c/z] | %w[c d] | true
+ %w[a/b/z] | %w[a/b] | false # we only consider refs ambiguous before the first slash
+ %w[a/b/z] | %w[a/b a] | true
+ %w[ab] | %w[abc/d a b] | false
end
with_them do
@@ -2596,7 +2596,7 @@ RSpec.describe Repository, feature_category: :source_code_management do
describe '#expire_branches_cache' do
it 'expires the cache' do
expect(repository).to receive(:expire_method_caches)
- .with(%i(branch_names merged_branch_names branch_count has_visible_content? has_ambiguous_refs?))
+ .with(%i[branch_names merged_branch_names branch_count has_visible_content? has_ambiguous_refs?])
.and_call_original
expect_next_instance_of(ProtectedBranches::CacheService) do |cache_service|
@@ -2630,7 +2630,7 @@ RSpec.describe Repository, feature_category: :source_code_management do
describe '#expire_tags_cache' do
it 'expires the cache' do
expect(repository).to receive(:expire_method_caches)
- .with(%i(tag_names tag_count has_ambiguous_refs?))
+ .with(%i[tag_names tag_count has_ambiguous_refs?])
.and_call_original
repository.expire_tags_cache
@@ -2889,7 +2889,7 @@ RSpec.describe Repository, feature_category: :source_code_management do
describe '#expire_statistics_caches' do
it 'expires the caches' do
expect(repository).to receive(:expire_method_caches)
- .with(%i(size recent_objects_size commit_count))
+ .with(%i[size recent_objects_size commit_count])
repository.expire_statistics_caches
end
@@ -3001,10 +3001,6 @@ RSpec.describe Repository, feature_category: :source_code_management do
it_behaves_like '#tree'
- describe '#tree? with Rugged enabled', :enable_rugged do
- it_behaves_like '#tree'
- end
-
describe '#size' do
context 'with a non-existing repository' do
it 'returns 0' do
@@ -3090,33 +3086,13 @@ RSpec.describe Repository, feature_category: :source_code_management do
describe '#refresh_method_caches' do
it 'refreshes the caches of the given types' do
expect(repository).to receive(:expire_method_caches)
- .with(%i(readme_path license_blob license_gitaly))
+ .with(%i[readme_path license_blob license_gitaly])
expect(repository).to receive(:readme_path)
expect(repository).to receive(:license_blob)
expect(repository).to receive(:license_gitaly)
- repository.refresh_method_caches(%i(readme license))
- end
- end
-
- describe '#gitlab_ci_yml_for' do
- let(:project) { create(:project, :repository) }
-
- before do
- repository.create_file(User.last, '.gitlab-ci.yml', 'CONTENT', message: 'Add .gitlab-ci.yml', branch_name: 'master')
- end
-
- context 'when there is a .gitlab-ci.yml at the commit' do
- it 'returns the content' do
- expect(repository.gitlab_ci_yml_for(repository.commit.sha)).to eq('CONTENT')
- end
- end
-
- context 'when there is no .gitlab-ci.yml at the commit' do
- it 'returns nil' do
- expect(repository.gitlab_ci_yml_for(repository.commit.parent.sha)).to be_nil
- end
+ repository.refresh_method_caches(%i[readme license])
end
end
@@ -3236,16 +3212,6 @@ RSpec.describe Repository, feature_category: :source_code_management do
end
end
- describe '#ancestor? with Rugged enabled', :enable_rugged do
- it 'calls out to the Rugged implementation' do
- allow_any_instance_of(Rugged).to receive(:merge_base).with(repository.commit.id, Gitlab::Git::BLANK_SHA).and_call_original
-
- repository.ancestor?(repository.commit.id, Gitlab::Git::BLANK_SHA)
- end
-
- it_behaves_like '#ancestor?'
- end
-
describe '#archive_metadata' do
let(:ref) { 'master' }
let(:storage_path) { '/tmp' }
@@ -3842,6 +3808,13 @@ RSpec.describe Repository, feature_category: :source_code_management do
it 'returns nil' do
expect(repository.get_patch_id('HEAD', 'HEAD')).to be_nil
end
+
+ it 'does not report the exception' do
+ expect(Gitlab::ErrorTracking)
+ .not_to receive(:track_exception)
+
+ repository.get_patch_id('HEAD', 'HEAD')
+ end
end
context 'when a Gitlab::Git::CommandError is raised' do
@@ -3851,7 +3824,7 @@ RSpec.describe Repository, feature_category: :source_code_management do
end
it 'returns nil' do
- expect(repository.get_patch_id('HEAD', 'HEAD')).to be_nil
+ expect(repository.get_patch_id('HEAD~', 'HEAD')).to be_nil
end
it 'reports the exception' do
@@ -3860,11 +3833,11 @@ RSpec.describe Repository, feature_category: :source_code_management do
.with(
instance_of(Gitlab::Git::CommandError),
project_id: repository.project.id,
- old_revision: 'HEAD',
+ old_revision: 'HEAD~',
new_revision: 'HEAD'
)
- repository.get_patch_id('HEAD', 'HEAD')
+ repository.get_patch_id('HEAD~', 'HEAD')
end
end
diff --git a/spec/models/service_desk/custom_email_credential_spec.rb b/spec/models/service_desk/custom_email_credential_spec.rb
index a990b77128e..dbf47a8f6a7 100644
--- a/spec/models/service_desk/custom_email_credential_spec.rb
+++ b/spec/models/service_desk/custom_email_credential_spec.rb
@@ -55,6 +55,39 @@ RSpec.describe ServiceDesk::CustomEmailCredential, feature_category: :service_de
end
end
+ describe '#delivery_options' do
+ let(:expected_attributes) do
+ {
+ address: 'smtp.example.com',
+ domain: 'example.com',
+ user_name: 'user@example.com',
+ port: 587,
+ password: 'supersecret',
+ authentication: nil
+ }
+ end
+
+ let(:setting) { build_stubbed(:service_desk_setting, project: project, custom_email: 'user@example.com') }
+
+ subject { credential.delivery_options }
+
+ before do
+ # credential.service_desk_setting is delegated to project and we only use build_stubbed
+ project.service_desk_setting = setting
+ end
+
+ it { is_expected.to include(expected_attributes) }
+
+ context 'when authentication is set' do
+ before do
+ credential.smtp_authentication = 'login'
+ expected_attributes[:authentication] = 'login'
+ end
+
+ it { is_expected.to include(expected_attributes) }
+ end
+ end
+
describe 'associations' do
it { is_expected.to belong_to(:project) }
diff --git a/spec/models/snippet_repository_spec.rb b/spec/models/snippet_repository_spec.rb
index 8048f255272..fe854f5d3ba 100644
--- a/spec/models/snippet_repository_spec.rb
+++ b/spec/models/snippet_repository_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe SnippetRepository do
+RSpec.describe SnippetRepository, feature_category: :snippets do
let_it_be(:user) { create(:user) }
let(:snippet) { create(:personal_snippet, :repository, author: user) }
@@ -68,6 +68,8 @@ RSpec.describe SnippetRepository do
expect(update_file_blob).not_to be_nil
end
+ expect(described_class.sticking).to receive(:stick)
+
expect do
snippet_repository.multi_files_action(user, data, **commit_opts)
end.not_to raise_error
diff --git a/spec/models/snippet_spec.rb b/spec/models/snippet_spec.rb
index ec2dfb2634f..ff1c5959cb0 100644
--- a/spec/models/snippet_spec.rb
+++ b/spec/models/snippet_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Snippet do
+RSpec.describe Snippet, feature_category: :source_code_management do
include FakeBlobHelpers
describe 'modules' do
@@ -26,6 +26,22 @@ RSpec.describe Snippet do
it { is_expected.to have_many(:repository_storage_moves).class_name('Snippets::RepositoryStorageMove').inverse_of(:container) }
end
+ describe 'scopes' do
+ describe '.with_repository_storage_moves' do
+ subject { described_class.with_repository_storage_moves }
+
+ let_it_be(:snippet) { create(:project_snippet) }
+
+ it { is_expected.to be_empty }
+
+ context 'when associated repository storage move exists' do
+ let!(:snippet_repository_storage_move) { create(:snippet_repository_storage_move, container: snippet) }
+
+ it { is_expected.to match_array([snippet]) }
+ end
+ end
+ end
+
describe 'validation' do
it { is_expected.to validate_presence_of(:author) }
@@ -614,7 +630,7 @@ RSpec.describe Snippet do
context 'when file does not exist' do
it 'removes nil values from the blobs array' do
- allow(snippet).to receive(:list_files).and_return(%w(LICENSE non_existent_snippet_file))
+ allow(snippet).to receive(:list_files).and_return(%w[LICENSE non_existent_snippet_file])
blobs = snippet.blobs
expect(blobs.count).to eq 1
diff --git a/spec/models/terraform/state_spec.rb b/spec/models/terraform/state_spec.rb
index fc0a6432149..df8051ebbc6 100644
--- a/spec/models/terraform/state_spec.rb
+++ b/spec/models/terraform/state_spec.rb
@@ -21,7 +21,7 @@ RSpec.describe Terraform::State, feature_category: :infrastructure_as_code do
describe '.ordered_by_name' do
let_it_be(:project) { create(:project) }
- let(:names) { %w(state_d state_b state_a state_c) }
+ let(:names) { %w[state_d state_b state_a state_c] }
subject { described_class.ordered_by_name }
diff --git a/spec/models/upload_spec.rb b/spec/models/upload_spec.rb
index 27e2060a94b..ff38edb73b6 100644
--- a/spec/models/upload_spec.rb
+++ b/spec/models/upload_spec.rb
@@ -266,7 +266,7 @@ RSpec.describe Upload do
it 'updates project statistics when upload is added' do
expect(ProjectCacheWorker).to receive(:perform_async)
- .with(project.id, [], [:uploads_size])
+ .with(project.id, [], ['uploads_size'])
subject.save!
end
@@ -275,7 +275,7 @@ RSpec.describe Upload do
subject.save!
expect(ProjectCacheWorker).to receive(:perform_async)
- .with(project.id, [], [:uploads_size])
+ .with(project.id, [], ['uploads_size'])
subject.destroy!
end
diff --git a/spec/models/user_detail_spec.rb b/spec/models/user_detail_spec.rb
index 428fd5470c3..b443988cde9 100644
--- a/spec/models/user_detail_spec.rb
+++ b/spec/models/user_detail_spec.rb
@@ -59,6 +59,27 @@ RSpec.describe UserDetail do
end
end
+ describe '#mastodon' do
+ it { is_expected.to validate_length_of(:mastodon).is_at_most(500) }
+
+ context 'when mastodon is set' do
+ let_it_be(:user_detail) { create(:user_detail) }
+
+ it 'accepts a valid mastodon username' do
+ user_detail.mastodon = '@robin@example.com'
+
+ expect(user_detail).to be_valid
+ end
+
+ it 'throws an error when mastodon username format is wrong' do
+ user_detail.mastodon = '@robin'
+
+ expect(user_detail).not_to be_valid
+ expect(user_detail.errors.full_messages).to match_array([_('Mastodon must contain only a mastodon username.')])
+ end
+ end
+ end
+
describe '#location' do
it { is_expected.to validate_length_of(:location).is_at_most(500) }
end
@@ -97,6 +118,7 @@ RSpec.describe UserDetail do
discord: '1234567890123456789',
linkedin: 'linkedin',
location: 'location',
+ mastodon: '@robin@example.com',
organization: 'organization',
skype: 'skype',
twitter: 'twitter',
@@ -117,6 +139,7 @@ RSpec.describe UserDetail do
it_behaves_like 'prevents `nil` value', :discord
it_behaves_like 'prevents `nil` value', :linkedin
it_behaves_like 'prevents `nil` value', :location
+ it_behaves_like 'prevents `nil` value', :mastodon
it_behaves_like 'prevents `nil` value', :organization
it_behaves_like 'prevents `nil` value', :skype
it_behaves_like 'prevents `nil` value', :twitter
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index 947d83badf6..fe229ce836f 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -44,6 +44,9 @@ RSpec.describe User, feature_category: :user_profile do
it { is_expected.to delegate_method(:time_display_relative).to(:user_preference) }
it { is_expected.to delegate_method(:time_display_relative=).to(:user_preference).with_arguments(:args) }
+ it { is_expected.to delegate_method(:time_display_format).to(:user_preference) }
+ it { is_expected.to delegate_method(:time_display_format=).to(:user_preference).with_arguments(:args) }
+
it { is_expected.to delegate_method(:show_whitespace_in_diffs).to(:user_preference) }
it { is_expected.to delegate_method(:show_whitespace_in_diffs=).to(:user_preference).with_arguments(:args) }
@@ -113,6 +116,9 @@ RSpec.describe User, feature_category: :user_profile do
it { is_expected.to delegate_method(:linkedin).to(:user_detail).allow_nil }
it { is_expected.to delegate_method(:linkedin=).to(:user_detail).with_arguments(:args).allow_nil }
+ it { is_expected.to delegate_method(:mastodon).to(:user_detail).allow_nil }
+ it { is_expected.to delegate_method(:mastodon=).to(:user_detail).with_arguments(:args).allow_nil }
+
it { is_expected.to delegate_method(:twitter).to(:user_detail).allow_nil }
it { is_expected.to delegate_method(:twitter=).to(:user_detail).with_arguments(:args).allow_nil }
@@ -130,6 +136,9 @@ RSpec.describe User, feature_category: :user_profile do
it { is_expected.to delegate_method(:email_reset_offered_at).to(:user_detail).allow_nil }
it { is_expected.to delegate_method(:email_reset_offered_at=).to(:user_detail).with_arguments(:args).allow_nil }
+
+ it { is_expected.to delegate_method(:project_authorizations_recalculated_at).to(:user_detail).allow_nil }
+ it { is_expected.to delegate_method(:project_authorizations_recalculated_at=).to(:user_detail).with_arguments(:args).allow_nil }
end
describe 'associations' do
@@ -1277,7 +1286,7 @@ RSpec.describe User, feature_category: :user_profile do
user = create(:user, username: 'CaMeLcAsEd')
user2 = create(:user, username: 'UPPERCASE')
- expect(described_class.by_username(%w(CAMELCASED uppercase)))
+ expect(described_class.by_username(%w[CAMELCASED uppercase]))
.to contain_exactly(user, user2)
end
@@ -1416,6 +1425,16 @@ RSpec.describe User, feature_category: :user_profile do
'ORDER BY "users"."current_sign_in_at" ASC NULLS LAST')
end
end
+
+ describe '.trusted' do
+ let_it_be(:trusted_user1) { create(:user, :trusted) }
+ let_it_be(:trusted_user2) { create(:user, :trusted) }
+ let_it_be(:user3) { create(:user) }
+
+ it 'returns only the trusted users' do
+ expect(described_class.trusted).to match_array([trusted_user1, trusted_user2])
+ end
+ end
end
context 'strip attributes' do
@@ -1824,7 +1843,7 @@ RSpec.describe User, feature_category: :user_profile do
end
context 'when the confirmation period has expired' do
- let(:confirmation_sent_at) { expired_confirmation_sent_at }
+ let(:confirmation_sent_at) { expired_confirmation_sent_at }
it_behaves_like 'unconfirmed user'
@@ -1842,7 +1861,7 @@ RSpec.describe User, feature_category: :user_profile do
end
context 'when the confirmation period has not expired' do
- let(:confirmation_sent_at) { extant_confirmation_sent_at }
+ let(:confirmation_sent_at) { extant_confirmation_sent_at }
it_behaves_like 'unconfirmed user'
@@ -2033,7 +2052,7 @@ RSpec.describe User, feature_category: :user_profile do
end
context 'when the confirmation period has expired' do
- let(:confirmation_sent_at) { expired_confirmation_sent_at }
+ let(:confirmation_sent_at) { expired_confirmation_sent_at }
it_behaves_like 'unconfirmed user'
it_behaves_like 'confirms the user on force_confirm'
@@ -2855,6 +2874,12 @@ RSpec.describe User, feature_category: :user_profile do
expect(described_class.filter_items('wop')).to include user
end
+
+ it 'filters by trusted' do
+ expect(described_class).to receive(:trusted).and_return([user])
+
+ expect(described_class.filter_items('trusted')).to include user
+ end
end
describe '.without_projects' do
@@ -3261,6 +3286,9 @@ RSpec.describe User, feature_category: :user_profile do
end
describe 'username matching' do
+ let_it_be(:named_john) { create(:user, name: 'John', username: 'abcd') }
+ let_it_be(:username_john) { create(:user, name: 'John Doe', username: 'john') }
+
it 'returns users with a matching username' do
expect(described_class.search(user.username)).to eq([user, user2])
end
@@ -3281,6 +3309,10 @@ RSpec.describe User, feature_category: :user_profile do
expect(described_class.search(user2.username.upcase)).to eq([user2])
end
+ it 'returns users with an exact matching username first' do
+ expect(described_class.search('John')).to eq([username_john, named_john])
+ end
+
it 'returns users with a exact matching username shorter than 3 chars' do
expect(described_class.search(user3.username)).to eq([user3])
end
@@ -5814,37 +5846,37 @@ RSpec.describe User, feature_category: :user_profile do
context 'oauth user' do
it 'returns true if name can be synced' do
- stub_omniauth_setting(sync_profile_attributes: %w(name location))
+ stub_omniauth_setting(sync_profile_attributes: %w[name location])
expect(user.sync_attribute?(:name)).to be_truthy
end
it 'returns true if email can be synced' do
- stub_omniauth_setting(sync_profile_attributes: %w(name email))
+ stub_omniauth_setting(sync_profile_attributes: %w[name email])
expect(user.sync_attribute?(:email)).to be_truthy
end
it 'returns true if location can be synced' do
- stub_omniauth_setting(sync_profile_attributes: %w(location email))
+ stub_omniauth_setting(sync_profile_attributes: %w[location email])
expect(user.sync_attribute?(:email)).to be_truthy
end
it 'returns false if name can not be synced' do
- stub_omniauth_setting(sync_profile_attributes: %w(location email))
+ stub_omniauth_setting(sync_profile_attributes: %w[location email])
expect(user.sync_attribute?(:name)).to be_falsey
end
it 'returns false if email can not be synced' do
- stub_omniauth_setting(sync_profile_attributes: %w(location name))
+ stub_omniauth_setting(sync_profile_attributes: %w[location name])
expect(user.sync_attribute?(:email)).to be_falsey
end
it 'returns false if location can not be synced' do
- stub_omniauth_setting(sync_profile_attributes: %w(name email))
+ stub_omniauth_setting(sync_profile_attributes: %w[name email])
expect(user.sync_attribute?(:location)).to be_falsey
end
@@ -5875,7 +5907,7 @@ RSpec.describe User, feature_category: :user_profile do
it 'returns true for email and location if ldap user and location declared as syncable' do
allow(user).to receive(:ldap_user?).and_return(true)
- stub_omniauth_setting(sync_profile_attributes: %w(location))
+ stub_omniauth_setting(sync_profile_attributes: %w[location])
expect(user.sync_attribute?(:name)).to be_falsey
expect(user.sync_attribute?(:email)).to be_truthy
diff --git a/spec/models/guest_spec.rb b/spec/models/users/anonymous_spec.rb
index 975b64cb855..f6151be6184 100644
--- a/spec/models/guest_spec.rb
+++ b/spec/models/users/anonymous_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Guest do
+RSpec.describe Users::Anonymous, feature_category: :system_access do
let_it_be(:public_project, reload: true) { create(:project, :public) }
let_it_be(:private_project) { create(:project, :private) }
let_it_be(:internal_project) { create(:project, :internal) }
diff --git a/spec/models/users/credit_card_validation_spec.rb b/spec/models/users/credit_card_validation_spec.rb
index 7faddb2384c..ae75020c768 100644
--- a/spec/models/users/credit_card_validation_spec.rb
+++ b/spec/models/users/credit_card_validation_spec.rb
@@ -3,6 +3,8 @@
require 'spec_helper'
RSpec.describe Users::CreditCardValidation, feature_category: :user_profile do
+ include CryptoHelpers
+
it { is_expected.to belong_to(:user) }
it { is_expected.to validate_length_of(:holder_name).is_at_most(50) }
@@ -206,12 +208,12 @@ RSpec.describe Users::CreditCardValidation, feature_category: :user_profile do
context 'when last_digits has a blank value' do
let(:last_digits) { ' ' }
- it { expect { save_credit_card_validation }.not_to change { credit_card_validation.last_digits_hash } }
+ it { expect(credit_card_validation).to be_invalid }
end
context 'when last_digits has a value' do
let(:last_digits) { 1111 }
- let(:expected_last_digits_hash) { Gitlab::CryptoHelper.sha256(last_digits) }
+ let(:expected_last_digits_hash) { sha256(last_digits) }
it 'assigns correct last_digits_hash value' do
expect { save_credit_card_validation }.to change {
@@ -240,7 +242,7 @@ RSpec.describe Users::CreditCardValidation, feature_category: :user_profile do
context 'when holder_name has a value' do
let(:holder_name) { 'John Smith' }
- let(:expected_holder_name_hash) { Gitlab::CryptoHelper.sha256(holder_name.downcase) }
+ let(:expected_holder_name_hash) { sha256(holder_name.downcase) }
it 'lowercases holder_name and assigns correct holder_name_hash value' do
expect { save_credit_card_validation }.to change {
@@ -269,7 +271,7 @@ RSpec.describe Users::CreditCardValidation, feature_category: :user_profile do
context 'when network has a value' do
let(:network) { 'Visa' }
- let(:expected_network_hash) { Gitlab::CryptoHelper.sha256(network.downcase) }
+ let(:expected_network_hash) { sha256(network.downcase) }
it 'lowercases network and assigns correct network_hash value' do
expect { save_credit_card_validation }.to change {
@@ -298,7 +300,7 @@ RSpec.describe Users::CreditCardValidation, feature_category: :user_profile do
context 'when expiration_date has a value' do
let(:expiration_date) { 1.year.from_now.to_date }
- let(:expected_expiration_date_hash) { Gitlab::CryptoHelper.sha256(expiration_date.to_s) }
+ let(:expected_expiration_date_hash) { sha256(expiration_date.to_s) }
it 'assigns correct expiration_date_hash value' do
expect { save_credit_card_validation }.to change {
diff --git a/spec/models/users/group_visit_spec.rb b/spec/models/users/group_visit_spec.rb
index 63c4631ad7d..241cb537fad 100644
--- a/spec/models/users/group_visit_spec.rb
+++ b/spec/models/users/group_visit_spec.rb
@@ -7,10 +7,6 @@ RSpec.describe Users::GroupVisit, feature_category: :navigation do
let_it_be(:user) { create(:user) }
let_it_be(:base_time) { DateTime.now }
- before do
- described_class.create!(entity_id: entity.id, user_id: user.id, visited_at: base_time)
- end
-
it_behaves_like 'namespace visits model'
it_behaves_like 'cleanup by a loose foreign key' do
@@ -22,4 +18,25 @@ RSpec.describe Users::GroupVisit, feature_category: :navigation do
let!(:model) { create(:group_visit, entity_id: entity.id, user_id: user.id, visited_at: base_time) }
let!(:parent) { user }
end
+
+ describe '#frecent_groups' do
+ let_it_be(:group1) { create(:group) }
+ let_it_be(:group2) { create(:group) }
+
+ before do
+ [
+ [group1.id, 1.day.ago],
+ [group2.id, 2.days.ago]
+ ].each do |id, datetime|
+ described_class.create!(entity_id: id, user_id: user.id, visited_at: datetime)
+ end
+ end
+
+ it "returns the associated frecently visited groups" do
+ expect(described_class.frecent_groups(user_id: user.id)).to eq([
+ group1,
+ group2
+ ])
+ end
+ end
end
diff --git a/spec/models/users/phone_number_validation_spec.rb b/spec/models/users/phone_number_validation_spec.rb
index 7ab461a4346..e41719d8ca3 100644
--- a/spec/models/users/phone_number_validation_spec.rb
+++ b/spec/models/users/phone_number_validation_spec.rb
@@ -2,7 +2,10 @@
require 'spec_helper'
-RSpec.describe Users::PhoneNumberValidation do
+RSpec.describe Users::PhoneNumberValidation, feature_category: :instance_resiliency do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:banned_user) { create(:user, :banned) }
+
it { is_expected.to belong_to(:user) }
it { is_expected.to belong_to(:banned_user) }
@@ -31,9 +34,6 @@ RSpec.describe Users::PhoneNumberValidation do
let_it_be(:international_dial_code) { 1 }
let_it_be(:phone_number) { '555' }
- let_it_be(:user) { create(:user) }
- let_it_be(:banned_user) { create(:user, :banned) }
-
subject(:related_to_banned_user?) do
described_class.related_to_banned_user?(international_dial_code, phone_number)
end
@@ -79,25 +79,25 @@ RSpec.describe Users::PhoneNumberValidation do
end
end
- describe '#for_user' do
- let_it_be(:user_1) { create(:user) }
- let_it_be(:user_2) { create(:user) }
+ describe 'scopes' do
+ let_it_be(:another_user) { create(:user) }
- let_it_be(:phone_number_record_1) { create(:phone_number_validation, user: user_1) }
- let_it_be(:phone_number_record_2) { create(:phone_number_validation, user: user_2) }
+ let_it_be(:phone_number_record_1) { create(:phone_number_validation, user: user, telesign_reference_xid: 'target') }
+ let_it_be(:phone_number_record_2) { create(:phone_number_validation, user: another_user) }
- context 'when multiple records exist for multiple users' do
- it 'returns the correct phone number record for user' do
- records = described_class.for_user(user_1.id)
+ describe '#for_user' do
+ context 'when multiple records exist for multiple users' do
+ it 'returns the correct phone number record for user' do
+ records = described_class.for_user(user.id)
- expect(records.count).to be(1)
- expect(records.first).to eq(phone_number_record_1)
+ expect(records.count).to be(1)
+ expect(records.first).to eq(phone_number_record_1)
+ end
end
end
end
describe '#validated?' do
- let_it_be(:user) { create(:user) }
let_it_be(:phone_number_record) { create(:phone_number_validation, user: user) }
context 'when phone number record is not validated' do
@@ -116,4 +116,20 @@ RSpec.describe Users::PhoneNumberValidation do
end
end
end
+
+ describe '.by_reference_id' do
+ let_it_be(:phone_number_record) { create(:phone_number_validation) }
+
+ let(:ref_id) { phone_number_record.telesign_reference_xid }
+
+ subject { described_class.by_reference_id(ref_id) }
+
+ it { is_expected.to eq phone_number_record }
+
+ context 'when there is no matching record' do
+ let(:ref_id) { 'does-not-exist' }
+
+ it { is_expected.to be_nil }
+ end
+ end
end
diff --git a/spec/models/users/project_visit_spec.rb b/spec/models/users/project_visit_spec.rb
index 38747bd6462..50e9ef02fd8 100644
--- a/spec/models/users/project_visit_spec.rb
+++ b/spec/models/users/project_visit_spec.rb
@@ -7,10 +7,6 @@ RSpec.describe Users::ProjectVisit, feature_category: :navigation do
let_it_be(:user) { create(:user) }
let_it_be(:base_time) { DateTime.now }
- before do
- described_class.create!(entity_id: entity.id, user_id: user.id, visited_at: base_time)
- end
-
it_behaves_like 'namespace visits model'
it_behaves_like 'cleanup by a loose foreign key' do
@@ -22,4 +18,25 @@ RSpec.describe Users::ProjectVisit, feature_category: :navigation do
let!(:model) { create(:project_visit, entity_id: entity.id, user_id: user.id, visited_at: base_time) }
let!(:parent) { user }
end
+
+ describe '#frecent_projects' do
+ let_it_be(:project1) { create(:project) }
+ let_it_be(:project2) { create(:project) }
+
+ before do
+ [
+ [project1.id, 1.day.ago],
+ [project2.id, 2.days.ago]
+ ].each do |id, datetime|
+ described_class.create!(entity_id: id, user_id: user.id, visited_at: datetime)
+ end
+ end
+
+ it "returns the associated frecently visited projects" do
+ expect(described_class.frecent_projects(user_id: user.id)).to eq([
+ project1,
+ project2
+ ])
+ end
+ end
end
diff --git a/spec/models/vs_code/settings/vs_code_setting_spec.rb b/spec/models/vs_code/settings/vs_code_setting_spec.rb
index d22cc815877..d61e89dc54b 100644
--- a/spec/models/vs_code/settings/vs_code_setting_spec.rb
+++ b/spec/models/vs_code/settings/vs_code_setting_spec.rb
@@ -11,10 +11,18 @@ RSpec.describe VsCode::Settings::VsCodeSetting, feature_category: :web_ide do
it { is_expected.to validate_presence_of(:content) }
end
+ describe 'validates the uniqueness of attributes' do
+ it { is_expected.to validate_uniqueness_of(:setting_type).scoped_to([:user_id]) }
+ end
+
describe 'relationship validation' do
it { is_expected.to belong_to(:user) }
end
+ describe 'settings type validation' do
+ it { is_expected.to validate_inclusion_of(:setting_type).in_array(VsCode::Settings::SETTINGS_TYPES) }
+ end
+
describe '.by_setting_type' do
subject { described_class.by_setting_type('settings') }
diff --git a/spec/models/web_ide_terminal_spec.rb b/spec/models/web_ide_terminal_spec.rb
index fc30bc18f68..505b7531db4 100644
--- a/spec/models/web_ide_terminal_spec.rb
+++ b/spec/models/web_ide_terminal_spec.rb
@@ -45,7 +45,7 @@ RSpec.describe WebIdeTerminal do
end
it 'returns services aliases' do
- expect(subject.services).to eq %w(postgres docker)
+ expect(subject.services).to eq %w[postgres docker]
end
end
@@ -55,7 +55,7 @@ RSpec.describe WebIdeTerminal do
end
it 'returns all aliases' do
- expect(subject.services).to eq %w(postgres docker ruby)
+ expect(subject.services).to eq %w[postgres docker ruby]
end
end
@@ -71,7 +71,7 @@ RSpec.describe WebIdeTerminal do
context 'when no image nor services' do
let(:config) do
- { script: %w(echo) }
+ { script: %w[echo] }
end
it 'returns an empty array' do
diff --git a/spec/models/wiki_page_spec.rb b/spec/models/wiki_page_spec.rb
index 2e1cb9d3d9b..f3cf8966b9a 100644
--- a/spec/models/wiki_page_spec.rb
+++ b/spec/models/wiki_page_spec.rb
@@ -58,6 +58,7 @@ RSpec.describe WikiPage, feature_category: :wiki do
let(:front_matter) { { title: 'Foo', slugs: %w[slug_a slug_b] } }
it { expect(wiki_page.front_matter).to eq(front_matter) }
+ it { expect(wiki_page.front_matter_title).to eq(front_matter[:title]) }
end
context 'the wiki page has front matter' do
@@ -1054,4 +1055,28 @@ RSpec.describe WikiPage, feature_category: :wiki do
)
end
end
+
+ describe "#human_title" do
+ context "with front matter title" do
+ let(:front_matter_title) { "abc" }
+ let(:content_with_front_matter_title) { "---\ntitle: #{front_matter_title}\n---\nHome Page" }
+ let(:wiki_page) { create(:wiki_page, container: container, content: content_with_front_matter_title) }
+
+ context "when wiki_front_matter_title enabled" do
+ it 'returns the front matter title' do
+ expect(wiki_page.human_title).to eq front_matter_title
+ end
+ end
+
+ context "when wiki_front_matter_title disabled" do
+ before do
+ stub_feature_flags(wiki_front_matter_title: false)
+ end
+
+ it 'returns the page title' do
+ expect(wiki_page.human_title).to eq wiki_page.title
+ end
+ end
+ end
+ end
end
diff --git a/spec/models/work_item_spec.rb b/spec/models/work_item_spec.rb
index 3294d53e364..476d346db10 100644
--- a/spec/models/work_item_spec.rb
+++ b/spec/models/work_item_spec.rb
@@ -402,6 +402,12 @@ RSpec.describe WorkItem, feature_category: :portfolio_management do
end
end
+ describe '#linked_items_keyset_order' do
+ subject { described_class.linked_items_keyset_order }
+
+ it { is_expected.to eq('"issue_links"."id" ASC') }
+ end
+
context 'with hierarchy' do
let_it_be(:type1) { create(:work_item_type, namespace: reusable_project.namespace) }
let_it_be(:type2) { create(:work_item_type, namespace: reusable_project.namespace) }
diff --git a/spec/models/zoom_meeting_spec.rb b/spec/models/zoom_meeting_spec.rb
index d3d75a19fed..b67a9d4a2ff 100644
--- a/spec/models/zoom_meeting_spec.rb
+++ b/spec/models/zoom_meeting_spec.rb
@@ -65,7 +65,7 @@ RSpec.describe ZoomMeeting do
context 'with non-Zoom URL' do
before do
- subject.url = %{https://non-zoom.url}
+ subject.url = %(https://non-zoom.url)
end
include_examples 'invalid Zoom URL'
@@ -73,7 +73,7 @@ RSpec.describe ZoomMeeting do
context 'with multiple Zoom-URLs' do
before do
- subject.url = %{https://zoom.us/j/123 https://zoom.us/j/456}
+ subject.url = %(https://zoom.us/j/123 https://zoom.us/j/456)
end
include_examples 'invalid Zoom URL'
diff --git a/spec/policies/abuse_report_policy_spec.rb b/spec/policies/abuse_report_policy_spec.rb
index b17b6886b9a..01ab29d1cf1 100644
--- a/spec/policies/abuse_report_policy_spec.rb
+++ b/spec/policies/abuse_report_policy_spec.rb
@@ -12,6 +12,7 @@ RSpec.describe AbuseReportPolicy, feature_category: :insider_threat do
it 'cannot read_abuse_report' do
expect(policy).to be_disallowed(:read_abuse_report)
+ expect(policy).to be_disallowed(:create_note)
end
end
@@ -20,6 +21,7 @@ RSpec.describe AbuseReportPolicy, feature_category: :insider_threat do
it 'can read_abuse_report' do
expect(policy).to be_allowed(:read_abuse_report)
+ expect(policy).to be_allowed(:create_note)
end
end
end
diff --git a/spec/policies/ci/build_policy_spec.rb b/spec/policies/ci/build_policy_spec.rb
index 6ab89daff82..ad568e60d5c 100644
--- a/spec/policies/ci/build_policy_spec.rb
+++ b/spec/policies/ci/build_policy_spec.rb
@@ -109,7 +109,8 @@ RSpec.describe Ci::BuildPolicy, feature_category: :continuous_integration do
allow(project).to receive(:branch_allows_collaboration?).and_return(true)
end
- it 'enables update_build if user is maintainer' do
+ it 'enables updates if user is maintainer', :aggregate_failures do
+ expect(policy).to be_allowed :cancel_build
expect(policy).to be_allowed :update_build
expect(policy).to be_allowed :update_commit_status
end
@@ -130,6 +131,7 @@ RSpec.describe Ci::BuildPolicy, feature_category: :continuous_integration do
end
it 'does not include ability to update build' do
+ expect(policy).to be_disallowed :cancel_build
expect(policy).to be_disallowed :update_build
end
@@ -139,6 +141,7 @@ RSpec.describe Ci::BuildPolicy, feature_category: :continuous_integration do
end
it 'does not include ability to update build' do
+ expect(policy).to be_disallowed :cancel_build
expect(policy).to be_disallowed :update_build
end
end
@@ -150,6 +153,7 @@ RSpec.describe Ci::BuildPolicy, feature_category: :continuous_integration do
end
it 'includes ability to update build' do
+ expect(policy).to be_allowed :cancel_build
expect(policy).to be_allowed :update_build
end
end
@@ -162,6 +166,7 @@ RSpec.describe Ci::BuildPolicy, feature_category: :continuous_integration do
end
it 'does not include ability to update build' do
+ expect(policy).to be_disallowed :cancel_build
expect(policy).to be_disallowed :update_build
end
end
@@ -172,6 +177,7 @@ RSpec.describe Ci::BuildPolicy, feature_category: :continuous_integration do
end
it 'includes ability to update build' do
+ expect(policy).to be_allowed :cancel_build
expect(policy).to be_allowed :update_build
end
end
diff --git a/spec/policies/ci/pipeline_policy_spec.rb b/spec/policies/ci/pipeline_policy_spec.rb
index e74bf8f7efa..7475cda5cf9 100644
--- a/spec/policies/ci/pipeline_policy_spec.rb
+++ b/spec/policies/ci/pipeline_policy_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Ci::PipelinePolicy, :models do
+RSpec.describe Ci::PipelinePolicy, :models, feature_category: :continuous_integration do
let(:user) { create(:user) }
let(:pipeline) { create(:ci_empty_pipeline, project: project) }
@@ -25,6 +25,7 @@ RSpec.describe Ci::PipelinePolicy, :models do
it 'does not include ability to update pipeline' do
expect(policy).to be_disallowed :update_pipeline
+ expect(policy).to be_disallowed :cancel_pipeline
end
end
@@ -35,6 +36,7 @@ RSpec.describe Ci::PipelinePolicy, :models do
it 'includes ability to update pipeline' do
expect(policy).to be_allowed :update_pipeline
+ expect(policy).to be_allowed :cancel_pipeline
end
end
@@ -47,6 +49,7 @@ RSpec.describe Ci::PipelinePolicy, :models do
it 'does not include ability to update pipeline' do
expect(policy).to be_disallowed :update_pipeline
+ expect(policy).to be_disallowed :cancel_pipeline
end
end
@@ -57,6 +60,7 @@ RSpec.describe Ci::PipelinePolicy, :models do
it 'includes ability to update pipeline' do
expect(policy).to be_allowed :update_pipeline
+ expect(policy).to be_allowed :cancel_pipeline
end
end
end
@@ -70,6 +74,7 @@ RSpec.describe Ci::PipelinePolicy, :models do
allow_any_instance_of(Project).to receive(:branch_allows_collaboration?).and_return(true)
expect(policy).to be_allowed :update_pipeline
+ expect(policy).to be_allowed :cancel_pipeline
end
end
diff --git a/spec/policies/concerns/policy_actor_spec.rb b/spec/policies/concerns/policy_actor_spec.rb
index 7fd9db67032..a38725d73d6 100644
--- a/spec/policies/concerns/policy_actor_spec.rb
+++ b/spec/policies/concerns/policy_actor_spec.rb
@@ -20,10 +20,4 @@ RSpec.describe PolicyActor, feature_category: :shared do
# initialized. So here we just use an instance
expect(build(:user).methods).to include(*methods)
end
-
- describe '#security_policy_bot?' do
- subject { PolicyActorTestClass.new.security_policy_bot? }
-
- it { is_expected.to eq(false) }
- end
end
diff --git a/spec/policies/global_policy_spec.rb b/spec/policies/global_policy_spec.rb
index 475e8f981dd..52fea8d782e 100644
--- a/spec/policies/global_policy_spec.rb
+++ b/spec/policies/global_policy_spec.rb
@@ -10,7 +10,6 @@ RSpec.describe GlobalPolicy, feature_category: :shared do
let_it_be(:service_account) { create(:user, :service_account) }
let_it_be(:migration_bot) { create(:user, :migration_bot) }
let_it_be(:security_bot) { create(:user, :security_bot) }
- let_it_be(:security_policy_bot) { create(:user, :security_policy_bot) }
let_it_be(:llm_bot) { create(:user, :llm_bot) }
let_it_be_with_reload(:current_user) { create(:user) }
let_it_be(:user) { create(:user) }
@@ -411,12 +410,6 @@ RSpec.describe GlobalPolicy, feature_category: :shared do
it { is_expected.to be_allowed(:access_git) }
end
- context 'security policy bot' do
- let(:current_user) { security_policy_bot }
-
- it { is_expected.to be_allowed(:access_git) }
- end
-
describe 'deactivated user' do
before do
current_user.deactivate
diff --git a/spec/policies/group_group_link_policy_spec.rb b/spec/policies/group_group_link_policy_spec.rb
new file mode 100644
index 00000000000..34bc1bc3bec
--- /dev/null
+++ b/spec/policies/group_group_link_policy_spec.rb
@@ -0,0 +1,63 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe GroupGroupLinkPolicy, feature_category: :system_access do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:group) { create(:group, :private) }
+ let_it_be(:group2) { create(:group, :private) }
+
+ let(:group_group_link) do
+ create(:group_group_link, shared_group: group, shared_with_group: group2)
+ end
+
+ subject(:policy) { described_class.new(user, group_group_link) }
+
+ describe 'read_shared_with_group' do
+ context 'when the user is a shared_group member' do
+ before_all do
+ group.add_guest(user)
+ end
+
+ it 'can read_shared_with_group' do
+ expect(policy).to be_allowed(:read_shared_with_group)
+ end
+ end
+
+ context 'when the user is not a shared_group member' do
+ context 'when user is not a shared_with_group member' do
+ context 'when the shared_with_group is private' do
+ it 'cannot read_shared_with_group' do
+ expect(policy).to be_disallowed(:read_shared_with_group)
+ end
+
+ context 'when the shared group is public' do
+ let_it_be(:group) { create(:group, :public) }
+
+ it 'cannot read_shared_with_group' do
+ expect(policy).to be_disallowed(:read_shared_with_group)
+ end
+ end
+ end
+
+ context 'when the shared_with_group is public' do
+ let_it_be(:group2) { create(:group, :public) }
+
+ it 'can read_shared_with_group' do
+ expect(policy).to be_allowed(:read_shared_with_group)
+ end
+ end
+ end
+
+ context 'when user is a shared_with_group member' do
+ before_all do
+ group2.add_developer(user)
+ end
+
+ it 'can read_shared_with_group' do
+ expect(policy).to be_allowed(:read_shared_with_group)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/policies/issue_policy_spec.rb b/spec/policies/issue_policy_spec.rb
index 743d96ee3dd..c19b7bcf9ea 100644
--- a/spec/policies/issue_policy_spec.rb
+++ b/spec/policies/issue_policy_spec.rb
@@ -8,16 +8,16 @@ RSpec.describe IssuePolicy, feature_category: :team_planning do
include ProjectHelpers
include UserHelpers
- let(:admin) { create(:user, :admin) }
- let(:guest) { create(:user) }
- let(:author) { create(:user) }
- let(:assignee) { create(:user) }
- let(:reporter) { create(:user) }
- let(:maintainer) { create(:user) }
- let(:owner) { create(:user) }
- let(:group) { create(:group, :public) }
- let(:reporter_from_group_link) { create(:user) }
- let(:non_member) { create(:user) }
+ let_it_be(:group) { create(:group, :public) }
+ let_it_be(:admin) { create(:user, :admin) }
+ let_it_be(:guest) { create(:user) }
+ let_it_be(:author) { create(:user) }
+ let_it_be(:assignee) { create(:user) }
+ let_it_be(:reporter) { create(:user) }
+ let_it_be(:maintainer) { create(:user) }
+ let_it_be(:owner) { create(:user) }
+ let_it_be(:reporter_from_group_link) { create(:user) }
+ let_it_be(:non_member) { create(:user) }
let(:support_bot) { Users::Internal.support_bot }
let(:alert_bot) { Users::Internal.alert_bot }
@@ -70,12 +70,12 @@ RSpec.describe IssuePolicy, feature_category: :team_planning do
end
context 'a private project' do
- let(:project) { create(:project, :private) }
- let(:issue) { create(:issue, project: project, assignees: [assignee], author: author) }
- let(:issue_no_assignee) { create(:issue, project: project) }
+ let_it_be(:project) { create(:project, :private) }
+ let_it_be_with_reload(:issue) { create(:issue, project: project, assignees: [assignee], author: author) }
+ let_it_be_with_reload(:issue_no_assignee) { create(:issue, project: project) }
let(:new_issue) { build(:issue, project: project, assignees: [assignee], author: author) }
- before do
+ before_all do
project.add_guest(guest)
project.add_guest(author)
project.add_guest(assignee)
@@ -86,6 +86,10 @@ RSpec.describe IssuePolicy, feature_category: :team_planning do
create(:project_group_link, group: group, project: project)
end
+ it 'allows guests to award emoji' do
+ expect(permissions(guest, issue)).to be_allowed(:award_emoji)
+ end
+
it 'allows guests to read issues' do
expect(permissions(guest, issue)).to be_allowed(:read_issue, :read_issue_iid, :admin_issue_relation)
expect(permissions(guest, issue)).to be_disallowed(:update_issue, :admin_issue, :set_issue_metadata, :set_confidentiality, :mark_note_as_internal)
@@ -191,13 +195,13 @@ RSpec.describe IssuePolicy, feature_category: :team_planning do
end
context 'a public project' do
- let(:project) { create(:project, :public) }
- let(:issue) { create(:issue, project: project, assignees: [assignee], author: author) }
- let(:issue_no_assignee) { create(:issue, project: project) }
- let(:issue_locked) { create(:issue, :locked, project: project, author: author, assignees: [assignee]) }
+ let_it_be_with_reload(:project) { create(:project, :public) }
+ let_it_be_with_reload(:issue) { create(:issue, project: project, assignees: [assignee], author: author) }
+ let_it_be_with_reload(:issue_no_assignee) { create(:issue, project: project) }
+ let_it_be_with_reload(:issue_locked) { create(:issue, :locked, project: project, author: author, assignees: [assignee]) }
let(:new_issue) { build(:issue, project: project) }
- before do
+ before_all do
project.add_guest(guest)
project.add_reporter(reporter)
project.add_maintainer(maintainer)
@@ -208,6 +212,10 @@ RSpec.describe IssuePolicy, feature_category: :team_planning do
create(:project_group_link, group: group, project: project)
end
+ it 'allows guests to award emoji' do
+ expect(permissions(guest, issue)).to be_allowed(:award_emoji)
+ end
+
it 'does not allow anonymous user to create todos' do
expect(permissions(nil, issue)).to be_allowed(:read_issue)
expect(permissions(nil, issue)).to be_disallowed(:create_todo, :update_subscription, :set_issue_metadata, :set_confidentiality, :admin_issue_relation)
@@ -304,12 +312,12 @@ RSpec.describe IssuePolicy, feature_category: :team_planning do
it_behaves_like 'support bot with service desk enabled'
context 'when issues are private' do
- before do
+ before_all do
project.project_feature.update!(issues_access_level: ProjectFeature::PRIVATE)
end
- let(:issue) { create(:issue, project: project, author: author) }
- let(:visitor) { create(:user) }
+ let_it_be_with_reload(:issue) { create(:issue, project: project, author: author) }
+ let_it_be(:visitor) { create(:user) }
it 'forbids visitors from viewing issues' do
expect(permissions(visitor, issue)).to be_disallowed(:read_issue)
@@ -423,10 +431,8 @@ RSpec.describe IssuePolicy, feature_category: :team_planning do
end
context 'when accounting for notes widget' do
- let(:policy) { described_class.new(reporter, note) }
-
context 'and notes widget is disabled for issue' do
- before do
+ before_all do
WorkItems::Type.default_by_type(:issue).widget_definitions.find_by_widget_type(:notes).update!(disabled: true)
end
@@ -450,6 +456,18 @@ RSpec.describe IssuePolicy, feature_category: :team_planning do
end
end
+ context 'when issue belongs to a group' do
+ let_it_be_with_reload(:issue) { create(:issue, :group_level, namespace: group) }
+
+ before_all do
+ group.add_guest(guest)
+ end
+
+ it 'allows guests to award emoji' do
+ expect(permissions(guest, issue)).to be_allowed(:award_emoji)
+ end
+ end
+
context 'with external authorization enabled' do
let(:user) { create(:user) }
let(:project) { create(:project, :public) }
diff --git a/spec/policies/project_group_link_policy_spec.rb b/spec/policies/project_group_link_policy_spec.rb
index 9461f33decb..1047d3acb1e 100644
--- a/spec/policies/project_group_link_policy_spec.rb
+++ b/spec/policies/project_group_link_policy_spec.rb
@@ -4,9 +4,8 @@ require 'spec_helper'
RSpec.describe ProjectGroupLinkPolicy, feature_category: :system_access do
let_it_be(:user) { create(:user) }
- let_it_be(:group) { create(:group, :private) }
let_it_be(:group2) { create(:group, :private) }
- let_it_be(:project) { create(:project, :private, group: group) }
+ let_it_be(:project) { create(:project, :private) }
let(:project_group_link) do
create(:project_group_link, project: project, group: group2, group_access: Gitlab::Access::DEVELOPER)
@@ -14,42 +13,92 @@ RSpec.describe ProjectGroupLinkPolicy, feature_category: :system_access do
subject(:policy) { described_class.new(user, project_group_link) }
- context 'when the user is a group owner' do
- before do
- project_group_link.group.add_owner(user)
- end
+ describe 'admin_project_group_link' do
+ context 'when the user is a group owner' do
+ before_all do
+ group2.add_owner(user)
+ end
- context 'when user is not project maintainer' do
- it 'can admin group_project_link' do
- expect(policy).to be_allowed(:admin_project_group_link)
+ context 'when user is not project maintainer' do
+ it 'can admin group_project_link' do
+ expect(policy).to be_allowed(:admin_project_group_link)
+ end
+ end
+
+ context 'when user is a project maintainer' do
+ before do
+ project_group_link.project.add_maintainer(user)
+ end
+
+ it 'can admin group_project_link' do
+ expect(policy).to be_allowed(:admin_project_group_link)
+ end
end
end
- context 'when user is a project maintainer' do
- before do
- project_group_link.project.add_maintainer(user)
+ context 'when user is not a group owner' do
+ context 'when user is a project maintainer' do
+ it 'can admin group_project_link' do
+ project_group_link.project.add_maintainer(user)
+
+ expect(policy).to be_allowed(:admin_project_group_link)
+ end
end
- it 'can admin group_project_link' do
- expect(policy).to be_allowed(:admin_project_group_link)
+ context 'when user is not a project maintainer' do
+ it 'cannot admin group_project_link' do
+ project_group_link.project.add_developer(user)
+
+ expect(policy).to be_disallowed(:admin_project_group_link)
+ end
end
end
end
- context 'when user is not a group owner' do
- context 'when user is a project maintainer' do
- it 'can admin group_project_link' do
- project_group_link.project.add_maintainer(user)
+ describe 'read_shared_with_group' do
+ context 'when the user is a project member' do
+ before_all do
+ project.add_guest(user)
+ end
- expect(policy).to be_allowed(:admin_project_group_link)
+ it 'can read_shared_with_group' do
+ expect(policy).to be_allowed(:read_shared_with_group)
end
end
- context 'when user is not a project maintainer' do
- it 'cannot admin group_project_link' do
- project_group_link.project.add_developer(user)
+ context 'when the user is not a project member' do
+ context 'when user is not a group member' do
+ context 'when the group is private' do
+ it 'cannot read_shared_with_group' do
+ expect(policy).to be_disallowed(:read_shared_with_group)
+ end
+
+ context 'when the project is public' do
+ let_it_be(:project) { create(:project, :public) }
+
+ it 'cannot read_shared_with_group' do
+ expect(policy).to be_disallowed(:read_shared_with_group)
+ end
+ end
+ end
+
+ context 'when the group is public' do
+ let_it_be(:group2) { create(:group, :public) }
+
+ it 'can read_shared_with_group' do
+ expect(policy).to be_allowed(:read_shared_with_group)
+ end
+ end
+ end
+
+ context 'when user is a group member' do
+ before_all do
+ group2.add_guest(user)
+ end
- expect(policy).to be_disallowed(:admin_project_group_link)
+ it 'can read_shared_with_group' do
+ expect(policy).to be_allowed(:read_shared_with_group)
+ end
end
end
end
diff --git a/spec/policies/project_policy_spec.rb b/spec/policies/project_policy_spec.rb
index 3de006d8c9b..fda889ff422 100644
--- a/spec/policies/project_policy_spec.rb
+++ b/spec/policies/project_policy_spec.rb
@@ -214,6 +214,7 @@ RSpec.describe ProjectPolicy, feature_category: :system_access do
it 'allows modify pipelines' do
expect_allowed(:create_pipeline)
expect_allowed(:update_pipeline)
+ expect_allowed(:cancel_pipeline)
expect_allowed(:create_pipeline_schedule)
end
end
@@ -224,6 +225,7 @@ RSpec.describe ProjectPolicy, feature_category: :system_access do
it 'disallows to modify pipelines' do
expect_disallowed(:create_pipeline)
expect_disallowed(:update_pipeline)
+ expect_disallowed(:cancel_pipeline)
expect_disallowed(:destroy_pipeline)
expect_disallowed(:create_pipeline_schedule)
end
@@ -285,7 +287,7 @@ RSpec.describe ProjectPolicy, feature_category: :system_access do
it 'disallows all permissions except pipeline when the feature is disabled' do
builds_permissions = [
- :create_build, :read_build, :update_build, :admin_build, :destroy_build,
+ :create_build, :read_build, :update_build, :cancel_build, :admin_build, :destroy_build,
:create_pipeline_schedule, :read_pipeline_schedule_variables, :update_pipeline_schedule, :admin_pipeline_schedule, :destroy_pipeline_schedule,
:create_environment, :read_environment, :update_environment, :admin_environment, :destroy_environment,
:create_deployment, :read_deployment, :update_deployment, :admin_deployment, :destroy_deployment
@@ -304,7 +306,7 @@ RSpec.describe ProjectPolicy, feature_category: :system_access do
it 'disallows pipeline and commit_status permissions' do
builds_permissions = [
- :create_pipeline, :update_pipeline, :admin_pipeline, :destroy_pipeline,
+ :create_pipeline, :update_pipeline, :cancel_pipeline, :admin_pipeline, :destroy_pipeline,
:create_commit_status, :update_commit_status, :admin_commit_status, :destroy_commit_status
]
@@ -316,8 +318,8 @@ RSpec.describe ProjectPolicy, feature_category: :system_access do
context 'repository feature' do
let(:repository_permissions) do
[
- :create_pipeline, :update_pipeline, :admin_pipeline, :destroy_pipeline,
- :create_build, :read_build, :update_build, :admin_build, :destroy_build,
+ :create_pipeline, :update_pipeline, :cancel_pipeline, :admin_pipeline, :destroy_pipeline,
+ :create_build, :read_build, :cancel_build, :update_build, :admin_build, :destroy_build,
:create_pipeline_schedule, :read_pipeline_schedule, :update_pipeline_schedule, :admin_pipeline_schedule, :destroy_pipeline_schedule,
:create_environment, :read_environment, :update_environment, :admin_environment, :destroy_environment,
:create_cluster, :read_cluster, :update_cluster, :admin_cluster,
@@ -389,7 +391,7 @@ RSpec.describe ProjectPolicy, feature_category: :system_access do
end
let(:maintainer_abilities) do
- %w(create_build create_pipeline)
+ %w[create_build create_pipeline]
end
it 'does not allow pushing code' do
@@ -411,7 +413,7 @@ RSpec.describe ProjectPolicy, feature_category: :system_access do
end
context 'importing members from another project' do
- %w(maintainer owner).each do |role|
+ %w[maintainer owner].each do |role|
context "with #{role}" do
let(:current_user) { send(role) }
@@ -419,7 +421,7 @@ RSpec.describe ProjectPolicy, feature_category: :system_access do
end
end
- %w(guest reporter developer anonymous).each do |role|
+ %w[guest reporter developer anonymous].each do |role|
context "with #{role}" do
let(:current_user) { send(role) }
@@ -441,7 +443,7 @@ RSpec.describe ProjectPolicy, feature_category: :system_access do
end
context 'importing work items' do
- %w(reporter developer maintainer owner).each do |role|
+ %w[reporter developer maintainer owner].each do |role|
context "with #{role}" do
let(:current_user) { send(role) }
@@ -449,7 +451,7 @@ RSpec.describe ProjectPolicy, feature_category: :system_access do
end
end
- %w(guest anonymous).each do |role|
+ %w[guest anonymous].each do |role|
context "with #{role}" do
let(:current_user) { send(role) }
@@ -471,7 +473,7 @@ RSpec.describe ProjectPolicy, feature_category: :system_access do
end
context 'reading usage quotas' do
- %w(maintainer owner).each do |role|
+ %w[maintainer owner].each do |role|
context "with #{role}" do
let(:current_user) { send(role) }
@@ -479,7 +481,7 @@ RSpec.describe ProjectPolicy, feature_category: :system_access do
end
end
- %w(guest reporter developer anonymous).each do |role|
+ %w[guest reporter developer anonymous].each do |role|
context "with #{role}" do
let(:current_user) { send(role) }
@@ -690,7 +692,7 @@ RSpec.describe ProjectPolicy, feature_category: :system_access do
it { is_expected.to be_disallowed(:fork_project) }
end
- %w(reporter developer maintainer).each do |role|
+ %w[reporter developer maintainer].each do |role|
context role do
let(:current_user) { send(role) }
@@ -789,7 +791,7 @@ RSpec.describe ProjectPolicy, feature_category: :system_access do
end
end
- %w(guest reporter developer maintainer owner).each do |role|
+ %w[guest reporter developer maintainer owner].each do |role|
context role do
let(:current_user) { send(role) }
@@ -817,7 +819,7 @@ RSpec.describe ProjectPolicy, feature_category: :system_access do
end
end
- %w(guest reporter developer maintainer owner).each do |role|
+ %w[guest reporter developer maintainer owner].each do |role|
context role do
let(:current_user) { send(role) }
@@ -1437,7 +1439,7 @@ RSpec.describe ProjectPolicy, feature_category: :system_access do
end
context 'security configuration feature' do
- %w(guest reporter).each do |role|
+ %w[guest reporter].each do |role|
context role do
let(:current_user) { send(role) }
@@ -1447,7 +1449,7 @@ RSpec.describe ProjectPolicy, feature_category: :system_access do
end
end
- %w(developer maintainer owner).each do |role|
+ %w[developer maintainer owner].each do |role|
context role do
let(:current_user) { send(role) }
@@ -1459,7 +1461,7 @@ RSpec.describe ProjectPolicy, feature_category: :system_access do
end
context 'infrastructure google cloud feature' do
- %w(guest reporter developer).each do |role|
+ %w[guest reporter developer].each do |role|
context role do
let(:current_user) { send(role) }
@@ -1469,7 +1471,7 @@ RSpec.describe ProjectPolicy, feature_category: :system_access do
end
end
- %w(maintainer owner).each do |role|
+ %w[maintainer owner].each do |role|
context role do
let(:current_user) { send(role) }
@@ -1481,7 +1483,7 @@ RSpec.describe ProjectPolicy, feature_category: :system_access do
end
context 'infrastructure aws feature' do
- %w(guest reporter developer).each do |role|
+ %w[guest reporter developer].each do |role|
context role do
let(:current_user) { send(role) }
@@ -1491,7 +1493,7 @@ RSpec.describe ProjectPolicy, feature_category: :system_access do
end
end
- %w(maintainer owner).each do |role|
+ %w[maintainer owner].each do |role|
context role do
let(:current_user) { send(role) }
@@ -1972,7 +1974,7 @@ RSpec.describe ProjectPolicy, feature_category: :system_access do
context 'project member' do
let(:project) { private_project }
- %w(guest reporter developer maintainer).each do |role|
+ %w[guest reporter developer maintainer].each do |role|
context role do
let(:current_user) { send(role) }
@@ -2001,7 +2003,7 @@ RSpec.describe ProjectPolicy, feature_category: :system_access do
end
context 'project member' do
- %w(guest reporter developer maintainer).each do |role|
+ %w[guest reporter developer maintainer].each do |role|
context role do
before do
project.add_member(current_user, role.to_sym)
@@ -2035,7 +2037,7 @@ RSpec.describe ProjectPolicy, feature_category: :system_access do
end
context 'project member' do
- %w(guest reporter developer maintainer).each do |role|
+ %w[guest reporter developer maintainer].each do |role|
context role do
before do
project.add_member(current_user, role.to_sym)
@@ -2065,7 +2067,7 @@ RSpec.describe ProjectPolicy, feature_category: :system_access do
let(:current_user) { create(:user) }
context 'project member' do
- %w(guest reporter developer maintainer).each do |role|
+ %w[guest reporter developer maintainer].each do |role|
context role do
before do
project.add_member(current_user, role.to_sym)
@@ -2393,44 +2395,48 @@ RSpec.describe ProjectPolicy, feature_category: :system_access do
developer_permissions + [:create_cluster, :read_cluster, :update_cluster, :admin_cluster, :admin_terraform_state, :admin_project_google_cloud]
end
- where(:project_visibility, :access_level, :role, :allowed) do
- :public | ProjectFeature::ENABLED | :maintainer | true
- :public | ProjectFeature::ENABLED | :developer | true
- :public | ProjectFeature::ENABLED | :guest | true
- :public | ProjectFeature::ENABLED | :anonymous | true
- :public | ProjectFeature::PRIVATE | :maintainer | true
- :public | ProjectFeature::PRIVATE | :developer | true
- :public | ProjectFeature::PRIVATE | :guest | true
- :public | ProjectFeature::PRIVATE | :anonymous | false
- :public | ProjectFeature::DISABLED | :maintainer | false
- :public | ProjectFeature::DISABLED | :developer | false
- :public | ProjectFeature::DISABLED | :guest | false
- :public | ProjectFeature::DISABLED | :anonymous | false
- :internal | ProjectFeature::ENABLED | :maintainer | true
- :internal | ProjectFeature::ENABLED | :developer | true
- :internal | ProjectFeature::ENABLED | :guest | true
- :internal | ProjectFeature::ENABLED | :anonymous | false
- :internal | ProjectFeature::PRIVATE | :maintainer | true
- :internal | ProjectFeature::PRIVATE | :developer | true
- :internal | ProjectFeature::PRIVATE | :guest | true
- :internal | ProjectFeature::PRIVATE | :anonymous | false
- :internal | ProjectFeature::DISABLED | :maintainer | false
- :internal | ProjectFeature::DISABLED | :developer | false
- :internal | ProjectFeature::DISABLED | :guest | false
- :internal | ProjectFeature::DISABLED | :anonymous | false
- :private | ProjectFeature::ENABLED | :maintainer | true
- :private | ProjectFeature::ENABLED | :developer | true
- :private | ProjectFeature::ENABLED | :guest | true
- :private | ProjectFeature::ENABLED | :anonymous | false
- :private | ProjectFeature::PRIVATE | :maintainer | true
- :private | ProjectFeature::PRIVATE | :developer | true
- :private | ProjectFeature::PRIVATE | :guest | true
- :private | ProjectFeature::PRIVATE | :anonymous | false
- :private | ProjectFeature::DISABLED | :maintainer | false
- :private | ProjectFeature::DISABLED | :developer | false
- :private | ProjectFeature::DISABLED | :guest | false
- :private | ProjectFeature::DISABLED | :anonymous | false
- end
+ shared_context 'with permission matrix' do
+ where(:project_visibility, :access_level, :role, :allowed) do
+ :public | ProjectFeature::ENABLED | :maintainer | true
+ :public | ProjectFeature::ENABLED | :developer | true
+ :public | ProjectFeature::ENABLED | :guest | true
+ :public | ProjectFeature::ENABLED | :anonymous | true
+ :public | ProjectFeature::PRIVATE | :maintainer | true
+ :public | ProjectFeature::PRIVATE | :developer | true
+ :public | ProjectFeature::PRIVATE | :guest | true
+ :public | ProjectFeature::PRIVATE | :anonymous | false
+ :public | ProjectFeature::DISABLED | :maintainer | false
+ :public | ProjectFeature::DISABLED | :developer | false
+ :public | ProjectFeature::DISABLED | :guest | false
+ :public | ProjectFeature::DISABLED | :anonymous | false
+ :internal | ProjectFeature::ENABLED | :maintainer | true
+ :internal | ProjectFeature::ENABLED | :developer | true
+ :internal | ProjectFeature::ENABLED | :guest | true
+ :internal | ProjectFeature::ENABLED | :anonymous | false
+ :internal | ProjectFeature::PRIVATE | :maintainer | true
+ :internal | ProjectFeature::PRIVATE | :developer | true
+ :internal | ProjectFeature::PRIVATE | :guest | true
+ :internal | ProjectFeature::PRIVATE | :anonymous | false
+ :internal | ProjectFeature::DISABLED | :maintainer | false
+ :internal | ProjectFeature::DISABLED | :developer | false
+ :internal | ProjectFeature::DISABLED | :guest | false
+ :internal | ProjectFeature::DISABLED | :anonymous | false
+ :private | ProjectFeature::ENABLED | :maintainer | true
+ :private | ProjectFeature::ENABLED | :developer | true
+ :private | ProjectFeature::ENABLED | :guest | true
+ :private | ProjectFeature::ENABLED | :anonymous | false
+ :private | ProjectFeature::PRIVATE | :maintainer | true
+ :private | ProjectFeature::PRIVATE | :developer | true
+ :private | ProjectFeature::PRIVATE | :guest | true
+ :private | ProjectFeature::PRIVATE | :anonymous | false
+ :private | ProjectFeature::DISABLED | :maintainer | false
+ :private | ProjectFeature::DISABLED | :developer | false
+ :private | ProjectFeature::DISABLED | :guest | false
+ :private | ProjectFeature::DISABLED | :anonymous | false
+ end
+ end
+
+ include_context 'with permission matrix'
with_them do
let(:current_user) { user_subject(role) }
@@ -2448,6 +2454,8 @@ RSpec.describe ProjectPolicy, feature_category: :system_access do
end
context 'when terraform state management is disabled' do
+ include_context 'with permission matrix'
+
before do
stub_config(terraform_state: { enabled: false })
end
@@ -2562,73 +2570,154 @@ RSpec.describe ProjectPolicy, feature_category: :system_access do
describe 'when user is authenticated via CI_JOB_TOKEN', :request_store do
using RSpec::Parameterized::TableSyntax
- where(:project_visibility, :user_role, :external_user, :scope_project_type, :token_scope_enabled, :result) do
- :private | :reporter | false | :same | true | true
- :private | :reporter | false | :same | false | true
- :private | :reporter | false | :different | true | false
- :private | :reporter | false | :different | false | true
- :private | :guest | false | :same | true | true
- :private | :guest | false | :same | false | true
- :private | :guest | false | :different | true | false
- :private | :guest | false | :different | false | true
-
- :internal | :reporter | false | :same | true | true
- :internal | :reporter | true | :same | true | true
- :internal | :reporter | false | :same | false | true
- :internal | :reporter | false | :different | true | true
- :internal | :reporter | true | :different | true | false
- :internal | :reporter | false | :different | false | true
- :internal | :guest | false | :same | true | true
- :internal | :guest | true | :same | true | true
- :internal | :guest | false | :same | false | true
- :internal | :guest | false | :different | true | true
- :internal | :guest | true | :different | true | false
- :internal | :guest | false | :different | false | true
-
- :public | :reporter | false | :same | true | true
- :public | :reporter | false | :same | false | true
- :public | :reporter | false | :different | true | true
- :public | :reporter | false | :different | false | true
- :public | :guest | false | :same | true | true
- :public | :guest | false | :same | false | true
- :public | :guest | false | :different | true | true
- :public | :guest | false | :different | false | true
- end
+ RSpec.shared_examples 'CI_JOB_TOKEN enforces the expected permissions' do
+ with_them do
+ let(:current_user) { public_send(user_role) }
+ let(:project) { public_send("#{project_visibility}_project") }
+ let(:job) { build_stubbed(:ci_build, project: scope_project, user: current_user) }
- with_them do
- let(:current_user) { public_send(user_role) }
- let(:project) { public_send("#{project_visibility}_project") }
- let(:job) { build_stubbed(:ci_build, project: scope_project, user: current_user) }
+ let(:scope_project) do
+ if scope_project_type == :same
+ project
+ else
+ create(:project, :private)
+ end
+ end
- let(:scope_project) do
- if scope_project_type == :same
- project
- else
- create(:project, :private)
+ before do
+ current_user.set_ci_job_token_scope!(job)
+ current_user.external = external_user
+ project.update!(
+ ci_outbound_job_token_scope_enabled: token_scope_enabled,
+ ci_inbound_job_token_scope_enabled: token_scope_enabled
+ )
+ scope_project.update!(
+ ci_outbound_job_token_scope_enabled: token_scope_enabled,
+ ci_inbound_job_token_scope_enabled: token_scope_enabled
+ )
+ end
+
+ it "enforces the expected permissions" do
+ if result
+ is_expected.to be_allowed("#{user_role}_access".to_sym)
+ else
+ is_expected.to be_disallowed("#{user_role}_access".to_sym)
+ end
end
end
+ end
- before do
- current_user.set_ci_job_token_scope!(job)
- current_user.external = external_user
- project.update!(
- ci_outbound_job_token_scope_enabled: token_scope_enabled,
- ci_inbound_job_token_scope_enabled: token_scope_enabled
- )
- scope_project.update!(
- ci_outbound_job_token_scope_enabled: token_scope_enabled,
- ci_inbound_job_token_scope_enabled: token_scope_enabled
- )
- end
-
- it "enforces the expected permissions" do
- if result
- is_expected.to be_allowed("#{user_role}_access".to_sym)
- else
- is_expected.to be_disallowed("#{user_role}_access".to_sym)
+ # Remove project_visibility on FF restrict_ci_job_token_for_public_and_internal_projects cleanup
+ where(:project_visibility, :user_role, :external_user, :scope_project_type, :token_scope_enabled, :result) do
+ :public | :reporter | false | :same | true | true
+ :public | :reporter | true | :same | true | true
+ :public | :reporter | false | :same | false | true
+ :public | :reporter | false | :different | true | false
+ :public | :reporter | true | :different | true | false
+ :public | :reporter | false | :different | false | true
+ :public | :guest | false | :same | true | true
+ :public | :guest | true | :same | true | true
+ :public | :guest | false | :same | false | true
+ :public | :guest | false | :different | true | false
+ :public | :guest | true | :different | true | false
+ :public | :guest | false | :different | false | true
+ end
+
+ include_examples "CI_JOB_TOKEN enforces the expected permissions"
+
+ context "when the project is public or internal and not on the allowlist" do
+ where(:feature, :permissions) do
+ :container_registry | [:build_read_container_image, :read_container_image]
+ :package_registry | [:read_package, :read_project]
+ :builds | [:read_commit_status]
+ :releases | [:read_release]
+ :environments | [:read_environment]
+ end
+
+ with_them do
+ let(:current_user) { developer }
+ let(:project) { public_project }
+ let(:job) { build_stubbed(:ci_build, project: scope_project, user: current_user) }
+ let_it_be(:scope_project) { create(:project, :private) }
+
+ before do
+ current_user.set_ci_job_token_scope!(job)
+
+ scope_project.update!(ci_inbound_job_token_scope_enabled: true)
+ end
+
+ it 'allows the permissions based on the feature access level' do
+ project.project_feature.update!("#{feature}_access_level": ProjectFeature::ENABLED)
+
+ permissions.each { |p| expect_allowed(p) }
+ end
+
+ it 'disallows the permissions if feature access level is restricted' do
+ project.project_feature.update!("#{feature}_access_level": ProjectFeature::PRIVATE)
+
+ permissions.each { |p| expect_disallowed(p) }
+ end
+
+ it 'disallows the permissions if feature access level is disabled' do
+ project.project_feature.update!("#{feature}_access_level": ProjectFeature::DISABLED)
+
+ permissions.each { |p| expect_disallowed(p) }
+ end
+
+ context "with restrict_ci_job_token_for_public_and_internal_projects disabled" do
+ before do
+ stub_feature_flags(restrict_ci_job_token_for_public_and_internal_projects: false)
+ end
+
+ it 'allows all permissions for private' do
+ project.project_feature.update!("#{feature}_access_level": ProjectFeature::PRIVATE)
+
+ permissions.each { |p| expect_allowed(p) }
+ end
end
end
end
+
+ context "with FF restrict_ci_job_token_for_public_and_internal_projects disabled" do
+ before do
+ stub_feature_flags(restrict_ci_job_token_for_public_and_internal_projects: false)
+ end
+
+ where(:project_visibility, :user_role, :external_user, :scope_project_type, :token_scope_enabled, :result) do
+ :private | :reporter | false | :same | true | true
+ :private | :reporter | false | :same | false | true
+ :private | :reporter | false | :different | true | false
+ :private | :reporter | false | :different | false | true
+ :private | :guest | false | :same | true | true
+ :private | :guest | false | :same | false | true
+ :private | :guest | false | :different | true | false
+ :private | :guest | false | :different | false | true
+
+ :internal | :reporter | false | :same | true | true
+ :internal | :reporter | true | :same | true | true
+ :internal | :reporter | false | :same | false | true
+ :internal | :reporter | false | :different | true | true
+ :internal | :reporter | true | :different | true | false
+ :internal | :reporter | false | :different | false | true
+ :internal | :guest | false | :same | true | true
+ :internal | :guest | true | :same | true | true
+ :internal | :guest | false | :same | false | true
+ :internal | :guest | false | :different | true | true
+ :internal | :guest | true | :different | true | false
+ :internal | :guest | false | :different | false | true
+
+ :public | :reporter | false | :same | true | true
+ :public | :reporter | false | :same | false | true
+ :public | :reporter | false | :different | true | true
+ :public | :reporter | false | :different | false | true
+ :public | :guest | false | :same | true | true
+ :public | :guest | false | :same | false | true
+ :public | :guest | false | :different | true | true
+ :public | :guest | false | :different | false | true
+ end
+
+ include_examples "CI_JOB_TOKEN enforces the expected permissions"
+ end
end
describe 'container_image policies' do
@@ -2825,7 +2914,7 @@ RSpec.describe ProjectPolicy, feature_category: :system_access do
end
end
- %w(guest reporter developer).each do |role|
+ %w[guest reporter developer].each do |role|
context role do
let(:current_user) { send(role) }
@@ -2833,7 +2922,7 @@ RSpec.describe ProjectPolicy, feature_category: :system_access do
end
end
- %w(maintainer owner).each do |role|
+ %w[maintainer owner].each do |role|
context role do
let(:current_user) { send(role) }
@@ -3212,9 +3301,23 @@ RSpec.describe ProjectPolicy, feature_category: :system_access do
end
describe 'add_catalog_resource' do
- let(:current_user) { owner }
+ using RSpec::Parameterized::TableSyntax
- specify { is_expected.to be_disallowed(:read_namespace_catalog) }
+ let(:current_user) { public_send(role) }
+
+ where(:role, :allowed) do
+ :owner | true
+ :maintainer | false
+ :developer | false
+ :reporter | false
+ :guest | false
+ end
+
+ with_them do
+ it do
+ expect(subject.can?(:add_catalog_resource)).to be(allowed)
+ end
+ end
end
describe 'read_model_registry' do
@@ -3237,6 +3340,28 @@ RSpec.describe ProjectPolicy, feature_category: :system_access do
end
end
+ describe 'write_model_registry' do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:ff_model_registry_enabled, :current_user, :allowed) do
+ true | ref(:reporter) | true
+ true | ref(:guest) | false
+ false | ref(:owner) | false
+ end
+ with_them do
+ before do
+ stub_feature_flags(model_registry: false)
+ stub_feature_flags(model_registry: project) if ff_model_registry_enabled
+ end
+
+ if params[:allowed]
+ it { expect_allowed(:write_model_registry) }
+ else
+ it { expect_disallowed(:write_model_registry) }
+ end
+ end
+ end
+
describe ':read_model_experiments' do
using RSpec::Parameterized::TableSyntax
diff --git a/spec/policies/user_policy_spec.rb b/spec/policies/user_policy_spec.rb
index 9a2caeb7435..bbfd231ed38 100644
--- a/spec/policies/user_policy_spec.rb
+++ b/spec/policies/user_policy_spec.rb
@@ -247,6 +247,32 @@ RSpec.describe UserPolicy do
end
end
+ describe ':read_user_organizations' do
+ context 'when user is admin' do
+ let(:current_user) { admin }
+
+ context 'when admin mode is enabled', :enable_admin_mode do
+ it { is_expected.to be_allowed(:read_user_organizations) }
+ end
+
+ context 'when admin mode is disabled' do
+ it { is_expected.not_to be_allowed(:read_user_organizations) }
+ end
+ end
+
+ context 'when user is not an admin' do
+ context 'requesting their own organizations' do
+ subject { described_class.new(current_user, current_user) }
+
+ it { is_expected.to be_allowed(:read_user_organizations) }
+ end
+
+ context "requesting a different user's orgnanizations" do
+ it { is_expected.not_to be_allowed(:read_user_organizations) }
+ end
+ end
+ end
+
describe ':read_user_email_address' do
context 'when user is admin' do
let(:current_user) { admin }
diff --git a/spec/policies/work_item_policy_spec.rb b/spec/policies/work_item_policy_spec.rb
index 568c375ce56..5b2eb8ec2e8 100644
--- a/spec/policies/work_item_policy_spec.rb
+++ b/spec/policies/work_item_policy_spec.rb
@@ -309,4 +309,76 @@ RSpec.describe WorkItemPolicy, feature_category: :team_planning do
end
end
end
+
+ describe 'read_note' do
+ context 'when work item is associated with a project' do
+ context 'when project is public' do
+ let(:work_item_subject) { public_work_item }
+
+ context 'when user is not a member of the project' do
+ let(:current_user) { non_member_user }
+
+ it { is_expected.to be_allowed(:read_note) }
+ end
+
+ context 'when user is a member of the project' do
+ let(:current_user) { guest_author }
+
+ it { is_expected.to be_allowed(:read_note) }
+
+ context 'when work_item is confidential' do
+ let(:work_item_subject) { create(:work_item, :confidential, project: project) }
+
+ it { is_expected.not_to be_allowed(:read_note) }
+ end
+ end
+ end
+ end
+
+ context 'when work item is associated with a group' do
+ context 'when group is public' do
+ let_it_be(:public_group) { create(:group, :public) }
+ let_it_be(:public_group_work_item) { create(:work_item, :group_level, namespace: public_group) }
+ let_it_be(:public_group_member) { create(:user).tap { |u| public_group.add_reporter(u) } }
+ let(:work_item_subject) { public_group_work_item }
+
+ context 'when user is not a member of the group' do
+ let(:current_user) { non_member_user }
+
+ it { is_expected.not_to be_allowed(:read_note) }
+ end
+
+ context 'when user is a member of the group' do
+ let(:current_user) { public_group_member }
+
+ it { is_expected.to be_allowed(:read_note) }
+ end
+ end
+
+ context 'when group is not public' do
+ let_it_be(:private_group) { create(:group, :private) }
+ let_it_be(:private_group_work_item) { create(:work_item, :group_level, namespace: private_group) }
+ let_it_be(:private_group_reporter) { create(:user).tap { |u| private_group.add_reporter(u) } }
+ let(:work_item_subject) { private_group_work_item }
+
+ context 'when user is not a member of the group' do
+ let(:current_user) { non_member_user }
+
+ it { is_expected.not_to be_allowed(:read_note) }
+ end
+
+ context 'when user is a member of the group' do
+ let(:current_user) { private_group_reporter }
+
+ it { is_expected.to be_allowed(:read_note) }
+
+ context 'when work_item is confidential' do
+ let(:work_item_subject) { create(:work_item, :group_level, :confidential, namespace: private_group) }
+
+ it { is_expected.to be_allowed(:read_note) }
+ end
+ end
+ end
+ end
+ end
end
diff --git a/spec/presenters/ci/pipeline_artifacts/code_coverage_presenter_spec.rb b/spec/presenters/ci/pipeline_artifacts/code_coverage_presenter_spec.rb
index e679f5fa144..2973040dd6a 100644
--- a/spec/presenters/ci/pipeline_artifacts/code_coverage_presenter_spec.rb
+++ b/spec/presenters/ci/pipeline_artifacts/code_coverage_presenter_spec.rb
@@ -12,7 +12,7 @@ RSpec.describe Ci::PipelineArtifacts::CodeCoveragePresenter do
context 'when code coverage has data' do
context 'when filenames is empty' do
- let(:filenames) { %w() }
+ let(:filenames) { %w[] }
it 'returns hash without coverage' do
expect(subject).to match(files: {})
@@ -20,7 +20,7 @@ RSpec.describe Ci::PipelineArtifacts::CodeCoveragePresenter do
end
context 'when filenames do not match code coverage data' do
- let(:filenames) { %w(demo.rb) }
+ let(:filenames) { %w[demo.rb] }
it 'returns hash without coverage' do
expect(subject).to match(files: {})
@@ -29,7 +29,7 @@ RSpec.describe Ci::PipelineArtifacts::CodeCoveragePresenter do
context 'when filenames matches code coverage data' do
context 'when asking for one filename' do
- let(:filenames) { %w(file_a.rb) }
+ let(:filenames) { %w[file_a.rb] }
it 'returns coverage for the given filename' do
expect(subject).to match(files: { "file_a.rb" => { "1" => 1, "2" => 1, "3" => 1 } })
@@ -37,7 +37,7 @@ RSpec.describe Ci::PipelineArtifacts::CodeCoveragePresenter do
end
context 'when asking for multiple filenames' do
- let(:filenames) { %w(file_a.rb file_b.rb) }
+ let(:filenames) { %w[file_a.rb file_b.rb] }
it 'returns coverage for a the given filenames' do
expect(subject).to match(
diff --git a/spec/presenters/ci/pipeline_artifacts/code_quality_mr_diff_presenter_spec.rb b/spec/presenters/ci/pipeline_artifacts/code_quality_mr_diff_presenter_spec.rb
index 99c82795210..f4f0990240d 100644
--- a/spec/presenters/ci/pipeline_artifacts/code_quality_mr_diff_presenter_spec.rb
+++ b/spec/presenters/ci/pipeline_artifacts/code_quality_mr_diff_presenter_spec.rb
@@ -13,7 +13,7 @@ RSpec.describe Ci::PipelineArtifacts::CodeQualityMrDiffPresenter, feature_catego
context 'when code quality has data' do
context 'when filenames is empty' do
- let(:filenames) { %w() }
+ let(:filenames) { %w[] }
it 'returns hash without quality' do
expect(quality_data).to match(files: {})
@@ -21,7 +21,7 @@ RSpec.describe Ci::PipelineArtifacts::CodeQualityMrDiffPresenter, feature_catego
end
context 'when filenames do not match code quality data' do
- let(:filenames) { %w(demo.rb) }
+ let(:filenames) { %w[demo.rb] }
it 'returns hash without quality' do
expect(quality_data).to match(files: {})
@@ -30,7 +30,7 @@ RSpec.describe Ci::PipelineArtifacts::CodeQualityMrDiffPresenter, feature_catego
context 'when filenames matches code quality data' do
context 'when asking for one filename' do
- let(:filenames) { %w(file_a.rb) }
+ let(:filenames) { %w[file_a.rb] }
it 'returns quality for the given filename' do
expect(quality_data).to match(
@@ -45,7 +45,7 @@ RSpec.describe Ci::PipelineArtifacts::CodeQualityMrDiffPresenter, feature_catego
end
context 'when asking for multiple filenames' do
- let(:filenames) { %w(file_a.rb file_b.rb) }
+ let(:filenames) { %w[file_a.rb file_b.rb] }
it 'returns quality for the given filenames' do
expect(quality_data).to match(
diff --git a/spec/presenters/clusters/cluster_presenter_spec.rb b/spec/presenters/clusters/cluster_presenter_spec.rb
index 755f1ea6078..aacb696a88e 100644
--- a/spec/presenters/clusters/cluster_presenter_spec.rb
+++ b/spec/presenters/clusters/cluster_presenter_spec.rb
@@ -123,7 +123,7 @@ RSpec.describe Clusters::ClusterPresenter do
'clusters-path': clusterable_presenter.index_path,
'dashboard-endpoint': clusterable_presenter.metrics_dashboard_path(cluster),
'documentation-path': help_page_path('user/infrastructure/clusters/manage/clusters_health'),
- 'add-dashboard-documentation-path': help_page_path('operations/metrics/dashboards/index.md', anchor: 'add-a-new-dashboard-to-your-project'),
+ 'add-dashboard-documentation-path': help_page_path('operations/metrics/dashboards/index', anchor: 'add-a-new-dashboard-to-your-project'),
'empty-getting-started-svg-path': match_asset_path('/assets/illustrations/monitoring/getting_started.svg'),
'empty-loading-svg-path': match_asset_path('/assets/illustrations/monitoring/loading.svg'),
'empty-no-data-svg-path': match_asset_path('/assets/illustrations/monitoring/no_data.svg'),
diff --git a/spec/presenters/ml/model_presenter_spec.rb b/spec/presenters/ml/model_presenter_spec.rb
index 88bfa9eb4c6..31bf4e7ad6c 100644
--- a/spec/presenters/ml/model_presenter_spec.rb
+++ b/spec/presenters/ml/model_presenter_spec.rb
@@ -6,6 +6,7 @@ RSpec.describe Ml::ModelPresenter, feature_category: :mlops do
let_it_be(:project) { build_stubbed(:project) }
let_it_be(:model1) { build_stubbed(:ml_models, project: project) }
let_it_be(:model2) { build_stubbed(:ml_models, :with_latest_version_and_package, project: project) }
+ let_it_be(:model3) { build_stubbed(:ml_models, :with_versions, project: project) }
describe '#latest_version_name' do
subject { model.present.latest_version_name }
@@ -25,6 +26,22 @@ RSpec.describe Ml::ModelPresenter, feature_category: :mlops do
end
end
+ describe '#version_count' do
+ subject { model3.present.version_count }
+
+ it { is_expected.to eq(2) }
+
+ context 'when model has precomputed version count' do
+ before do
+ allow(model3).to receive(:version_count).and_return(1)
+ end
+
+ it 'returns the value of model version count' do
+ is_expected.to eq(1)
+ end
+ end
+ end
+
describe '#latest_package_path' do
subject { model.present.latest_package_path }
@@ -41,6 +58,22 @@ RSpec.describe Ml::ModelPresenter, feature_category: :mlops do
end
end
+ describe '#latest_version_path' do
+ subject { model.present.latest_version_path }
+
+ context 'when model version does not have package' do
+ let(:model) { model1 }
+
+ it { is_expected.to be_nil }
+ end
+
+ context 'when latest model version has package' do
+ let(:model) { model2 }
+
+ it { is_expected.to eq("/#{project.full_path}/-/ml/models/#{model.id}/versions/#{model.latest_version.id}") }
+ end
+ end
+
describe '#path' do
subject { model1.present.path }
diff --git a/spec/presenters/ml/model_version_presenter_spec.rb b/spec/presenters/ml/model_version_presenter_spec.rb
new file mode 100644
index 00000000000..7624aaffc7a
--- /dev/null
+++ b/spec/presenters/ml/model_version_presenter_spec.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Ml::ModelVersionPresenter, feature_category: :mlops do
+ let_it_be(:project) { build_stubbed(:project) }
+ let_it_be(:model) { build_stubbed(:ml_models, name: 'a_model', project: project) }
+ let_it_be(:model_version) { build_stubbed(:ml_model_versions, :with_package, model: model, version: '1.1.1') }
+ let_it_be(:presenter) { model_version.present }
+
+ describe '.display_name' do
+ subject { presenter.display_name }
+
+ it { is_expected.to eq('a_model / 1.1.1') }
+ end
+
+ describe '#path' do
+ subject { presenter.path }
+
+ it { is_expected.to eq("/#{project.full_path}/-/ml/models/#{model.id}/versions/#{model_version.id}") }
+ end
+
+ describe '#package_path' do
+ subject { presenter.package_path }
+
+ it { is_expected.to eq("/#{project.full_path}/-/packages/#{model_version.package_id}") }
+ end
+end
diff --git a/spec/presenters/packages/nuget/packages_metadata_presenter_spec.rb b/spec/presenters/packages/nuget/packages_metadata_presenter_spec.rb
index 38b33a0ec4b..c7bdff9dd61 100644
--- a/spec/presenters/packages/nuget/packages_metadata_presenter_spec.rb
+++ b/spec/presenters/packages/nuget/packages_metadata_presenter_spec.rb
@@ -17,7 +17,7 @@ RSpec.describe Packages::Nuget::PackagesMetadataPresenter, feature_category: :pa
end
describe '#items' do
- let(:tag_names) { %w(tag1 tag2) }
+ let(:tag_names) { %w[tag1 tag2] }
subject { presenter.items }
diff --git a/spec/presenters/packages/nuget/search_results_presenter_spec.rb b/spec/presenters/packages/nuget/search_results_presenter_spec.rb
index e761a8740ef..7501cb75682 100644
--- a/spec/presenters/packages/nuget/search_results_presenter_spec.rb
+++ b/spec/presenters/packages/nuget/search_results_presenter_spec.rb
@@ -25,7 +25,7 @@ RSpec.describe Packages::Nuget::SearchResultsPresenter, feature_category: :packa
it 'returns the proper data structure' do
expect(data.size).to eq 3
pkg_a, pkg_b, pkg_c = data
- expect_package_result(pkg_a, package_a.name, [package_a.version], %w(tag1 tag2), with_metadatum: true)
+ expect_package_result(pkg_a, package_a.name, [package_a.version], %w[tag1 tag2], with_metadatum: true)
expect_package_result(pkg_b, packages_b.first.name, packages_b.map(&:version))
expect_package_result(pkg_c, packages_c.first.name, packages_c.map(&:version))
end
diff --git a/spec/presenters/project_presenter_spec.rb b/spec/presenters/project_presenter_spec.rb
index 42c43a59fe2..48db41ea8e3 100644
--- a/spec/presenters/project_presenter_spec.rb
+++ b/spec/presenters/project_presenter_spec.rb
@@ -872,4 +872,26 @@ RSpec.describe ProjectPresenter do
end
end
end
+
+ describe '#has_review_app?' do
+ subject { presenter.has_review_app? }
+
+ let_it_be(:project) { create(:project, :repository) }
+
+ context 'when review apps exist' do
+ let!(:environment) do
+ create(:environment, :with_review_app, project: project)
+ end
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'when review apps do not exist' do
+ let!(:environment) do
+ create(:environment, project: project)
+ end
+
+ it { is_expected.to be_falsey }
+ end
+ end
end
diff --git a/spec/presenters/projects/security/configuration_presenter_spec.rb b/spec/presenters/projects/security/configuration_presenter_spec.rb
index beabccf6639..fcd170dfd66 100644
--- a/spec/presenters/projects/security/configuration_presenter_spec.rb
+++ b/spec/presenters/projects/security/configuration_presenter_spec.rb
@@ -177,7 +177,7 @@ RSpec.describe Projects::Security::ConfigurationPresenter, feature_category: :so
let!(:ci_config) do
project.repository.create_file(
project.creator,
- Gitlab::FileDetector::PATTERNS[:gitlab_ci],
+ project.ci_config_path_or_default,
'contents go here',
message: 'test',
branch_name: 'master')
diff --git a/spec/presenters/user_presenter_spec.rb b/spec/presenters/user_presenter_spec.rb
index d1124d73dbd..fcbadf40bc9 100644
--- a/spec/presenters/user_presenter_spec.rb
+++ b/spec/presenters/user_presenter_spec.rb
@@ -61,28 +61,14 @@ RSpec.describe UserPresenter do
let_it_be(:other_user) { create(:user) }
let_it_be(:saved_reply) { create(:saved_reply, user: user) }
- context 'when feature is disabled' do
- before do
- stub_feature_flags(saved_replies: false)
- end
+ context 'when user has no permission to read saved replies' do
+ let(:current_user) { other_user }
it { expect(presenter.saved_replies).to eq(::Users::SavedReply.none) }
end
- context 'when feature is enabled' do
- before do
- stub_feature_flags(saved_replies: current_user)
- end
-
- context 'when user has no permission to read saved replies' do
- let(:current_user) { other_user }
-
- it { expect(presenter.saved_replies).to eq(::Users::SavedReply.none) }
- end
-
- context 'when user has permission to read saved replies' do
- it { expect(presenter.saved_replies).to eq([saved_reply]) }
- end
+ context 'when user has permission to read saved replies' do
+ it { expect(presenter.saved_replies).to eq([saved_reply]) }
end
end
end
diff --git a/spec/requests/acme_challenges_controller_spec.rb b/spec/requests/acme_challenges_controller_spec.rb
deleted file mode 100644
index f37aefed488..00000000000
--- a/spec/requests/acme_challenges_controller_spec.rb
+++ /dev/null
@@ -1,9 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe AcmeChallengesController, type: :request, feature_category: :pages do
- it_behaves_like 'Base action controller' do
- subject(:request) { get acme_challenge_path }
- end
-end
diff --git a/spec/requests/admin/users_controller_spec.rb b/spec/requests/admin/users_controller_spec.rb
index e525d615b50..2f8025691f4 100644
--- a/spec/requests/admin/users_controller_spec.rb
+++ b/spec/requests/admin/users_controller_spec.rb
@@ -74,4 +74,54 @@ RSpec.describe Admin::UsersController, :enable_admin_mode, feature_category: :us
expect { request }.to change { user.reload.access_locked? }.from(true).to(false)
end
end
+
+ describe 'PUT #trust' do
+ subject(:request) { put trust_admin_user_path(user) }
+
+ it 'trusts the user' do
+ expect { request }.to change { user.reload.trusted? }.from(false).to(true)
+ end
+
+ context 'when setting trust fails' do
+ before do
+ allow_next_instance_of(Users::TrustService) do |instance|
+ allow(instance).to receive(:execute).and_return({ status: :failed })
+ end
+ end
+
+ it 'displays a flash alert' do
+ request
+
+ expect(response).to redirect_to(admin_user_path(user))
+ expect(flash[:alert]).to eq(s_('Error occurred. User was not updated'))
+ end
+ end
+ end
+
+ describe 'PUT #untrust' do
+ before do
+ user.custom_attributes.create!(key: UserCustomAttribute::TRUSTED_BY, value: "placeholder")
+ end
+
+ subject(:request) { put untrust_admin_user_path(user) }
+
+ it 'trusts the user' do
+ expect { request }.to change { user.reload.trusted? }.from(true).to(false)
+ end
+
+ context 'when untrusting fails' do
+ before do
+ allow_next_instance_of(Users::UntrustService) do |instance|
+ allow(instance).to receive(:execute).and_return({ status: :failed })
+ end
+ end
+
+ it 'displays a flash alert' do
+ request
+
+ expect(response).to redirect_to(admin_user_path(user))
+ expect(flash[:alert]).to eq(s_('Error occurred. User was not updated'))
+ end
+ end
+ end
end
diff --git a/spec/requests/api/badges_spec.rb b/spec/requests/api/badges_spec.rb
index 0b340b95b20..92c5e90027b 100644
--- a/spec/requests/api/badges_spec.rb
+++ b/spec/requests/api/badges_spec.rb
@@ -363,7 +363,7 @@ RSpec.describe API::Badges, feature_category: :groups_and_projects do
end
describe 'Endpoints' do
- %w(project group).each do |source_type|
+ %w[project group].each do |source_type|
it_behaves_like 'GET /:sources/:id/badges', source_type
it_behaves_like 'GET /:sources/:id/badges/:badge_id', source_type
it_behaves_like 'GET /:sources/:id/badges/render', source_type
diff --git a/spec/requests/api/bulk_imports_spec.rb b/spec/requests/api/bulk_imports_spec.rb
index d3d4a723616..bbc01b30361 100644
--- a/spec/requests/api/bulk_imports_spec.rb
+++ b/spec/requests/api/bulk_imports_spec.rb
@@ -394,7 +394,7 @@ RSpec.describe API::BulkImports, feature_category: :importers do
expect(response).to have_gitlab_http_status(:ok)
expect(json_response.pluck('id')).to contain_exactly(entity_3.id)
- expect(json_response.first['failures'].first['exception_class']).to eq(failure_3.exception_class)
+ expect(json_response.first['failures'].first['exception_message']).to eq(failure_3.exception_message)
end
it_behaves_like 'disabled feature'
@@ -420,4 +420,17 @@ RSpec.describe API::BulkImports, feature_category: :importers do
expect(response).to have_gitlab_http_status(:unauthorized)
end
end
+
+ describe 'GET /bulk_imports/:id/entities/:entity_id/failures' do
+ let(:request) { get api("/bulk_imports/#{import_2.id}/entities/#{entity_3.id}/failures", user) }
+
+ it 'returns specified entity failures' do
+ request
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response.first['exception_message']).to eq(failure_3.exception_message)
+ end
+
+ it_behaves_like 'disabled feature'
+ end
end
diff --git a/spec/requests/api/ci/job_artifacts_spec.rb b/spec/requests/api/ci/job_artifacts_spec.rb
index 6f4e7fd66ed..b96ba356855 100644
--- a/spec/requests/api/ci/job_artifacts_spec.rb
+++ b/spec/requests/api/ci/job_artifacts_spec.rb
@@ -14,9 +14,7 @@ RSpec.describe API::Ci::JobArtifacts, feature_category: :build_artifacts do
end
let_it_be(:pipeline, reload: true) do
- create(:ci_pipeline, project: project,
- sha: project.commit.id,
- ref: project.default_branch)
+ create(:ci_pipeline, project: project, sha: project.commit.id, ref: project.default_branch)
end
let(:user) { create(:user) }
@@ -179,8 +177,7 @@ RSpec.describe API::Ci::JobArtifacts, feature_category: :build_artifacts do
context 'when project is public' do
it 'allows to access artifacts' do
- project.update_column(:visibility_level,
- Gitlab::VisibilityLevel::PUBLIC)
+ project.update_column(:visibility_level, Gitlab::VisibilityLevel::PUBLIC)
project.update_column(:public_builds, true)
get_artifact_file(artifact)
@@ -193,8 +190,7 @@ RSpec.describe API::Ci::JobArtifacts, feature_category: :build_artifacts do
let(:job) { create(:ci_build, :artifacts, :with_private_artifacts_config, pipeline: pipeline) }
it 'rejects access to artifacts' do
- project.update_column(:visibility_level,
- Gitlab::VisibilityLevel::PUBLIC)
+ project.update_column(:visibility_level, Gitlab::VisibilityLevel::PUBLIC)
project.update_column(:public_builds, true)
get_artifact_file(artifact)
@@ -208,8 +204,7 @@ RSpec.describe API::Ci::JobArtifacts, feature_category: :build_artifacts do
end
it 'allows access to artifacts' do
- project.update_column(:visibility_level,
- Gitlab::VisibilityLevel::PUBLIC)
+ project.update_column(:visibility_level, Gitlab::VisibilityLevel::PUBLIC)
project.update_column(:public_builds, true)
get_artifact_file(artifact)
@@ -221,8 +216,7 @@ RSpec.describe API::Ci::JobArtifacts, feature_category: :build_artifacts do
context 'when project is public with builds access disabled' do
it 'rejects access to artifacts' do
- project.update_column(:visibility_level,
- Gitlab::VisibilityLevel::PUBLIC)
+ project.update_column(:visibility_level, Gitlab::VisibilityLevel::PUBLIC)
project.update_column(:public_builds, false)
get_artifact_file(artifact)
@@ -233,8 +227,7 @@ RSpec.describe API::Ci::JobArtifacts, feature_category: :build_artifacts do
context 'when project is private' do
it 'rejects access and hides existence of artifacts' do
- project.update_column(:visibility_level,
- Gitlab::VisibilityLevel::PRIVATE)
+ project.update_column(:visibility_level, Gitlab::VisibilityLevel::PRIVATE)
project.update_column(:public_builds, true)
get_artifact_file(artifact)
@@ -254,8 +247,7 @@ RSpec.describe API::Ci::JobArtifacts, feature_category: :build_artifacts do
expect(response).to have_gitlab_http_status(:ok)
expect(response.headers.to_h)
- .to include('Content-Type' => 'application/json',
- 'Gitlab-Workhorse-Send-Data' => /artifacts-entry/)
+ .to include('Content-Type' => 'application/json', 'Gitlab-Workhorse-Send-Data' => /artifacts-entry/)
expect(response.headers.to_h)
.not_to include('Gitlab-Workhorse-Detect-Content-Type' => 'true')
expect(response.parsed_body).to be_empty
@@ -404,10 +396,12 @@ RSpec.describe API::Ci::JobArtifacts, feature_category: :build_artifacts do
end
before do
- stub_object_storage_uploader(config: Gitlab.config.artifacts.object_store,
- uploader: JobArtifactUploader,
- proxy_download: proxy_download,
- cdn: cdn_config)
+ stub_object_storage_uploader(
+ config: Gitlab.config.artifacts.object_store,
+ uploader: JobArtifactUploader,
+ proxy_download: proxy_download,
+ cdn: cdn_config
+ )
allow(Gitlab::ApplicationContext).to receive(:push).and_call_original
end
@@ -624,10 +618,11 @@ RSpec.describe API::Ci::JobArtifacts, feature_category: :build_artifacts do
it 'allows to access artifacts', :sidekiq_might_not_need_inline do
expect(response).to have_gitlab_http_status(:ok)
- expect(response.headers.to_h)
- .to include('Content-Type' => 'application/json',
- 'Gitlab-Workhorse-Send-Data' => /artifacts-entry/,
- 'Gitlab-Workhorse-Detect-Content-Type' => 'true')
+ expect(response.headers.to_h).to include(
+ 'Content-Type' => 'application/json',
+ 'Gitlab-Workhorse-Send-Data' => /artifacts-entry/,
+ 'Gitlab-Workhorse-Detect-Content-Type' => 'true'
+ )
end
end
@@ -695,10 +690,11 @@ RSpec.describe API::Ci::JobArtifacts, feature_category: :build_artifacts do
get_artifact_file(artifact)
expect(response).to have_gitlab_http_status(:ok)
- expect(response.headers.to_h)
- .to include('Content-Type' => 'application/json',
- 'Gitlab-Workhorse-Send-Data' => /artifacts-entry/,
- 'Gitlab-Workhorse-Detect-Content-Type' => 'true')
+ expect(response.headers.to_h).to include(
+ 'Content-Type' => 'application/json',
+ 'Gitlab-Workhorse-Send-Data' => /artifacts-entry/,
+ 'Gitlab-Workhorse-Detect-Content-Type' => 'true'
+ )
expect(response.parsed_body).to be_empty
end
end
@@ -713,10 +709,11 @@ RSpec.describe API::Ci::JobArtifacts, feature_category: :build_artifacts do
get_artifact_file(artifact, 'improve/awesome')
expect(response).to have_gitlab_http_status(:ok)
- expect(response.headers.to_h)
- .to include('Content-Type' => 'application/json',
- 'Gitlab-Workhorse-Send-Data' => /artifacts-entry/,
- 'Gitlab-Workhorse-Detect-Content-Type' => 'true')
+ expect(response.headers.to_h).to include(
+ 'Content-Type' => 'application/json',
+ 'Gitlab-Workhorse-Send-Data' => /artifacts-entry/,
+ 'Gitlab-Workhorse-Detect-Content-Type' => 'true'
+ )
end
end
@@ -765,8 +762,15 @@ RSpec.describe API::Ci::JobArtifacts, feature_category: :build_artifacts do
context 'artifacts did not expire' do
let(:job) do
- create(:ci_build, :trace_artifact, :artifacts, :success,
- project: project, pipeline: pipeline, artifacts_expire_at: Time.now + 7.days)
+ create(
+ :ci_build,
+ :trace_artifact,
+ :artifacts,
+ :success,
+ project: project,
+ pipeline: pipeline,
+ artifacts_expire_at: Time.now + 7.days
+ )
end
it 'keeps artifacts' do
diff --git a/spec/requests/api/ci/jobs_spec.rb b/spec/requests/api/ci/jobs_spec.rb
index 41e35de189e..382aabd45a1 100644
--- a/spec/requests/api/ci/jobs_spec.rb
+++ b/spec/requests/api/ci/jobs_spec.rb
@@ -14,9 +14,7 @@ RSpec.describe API::Ci::Jobs, feature_category: :continuous_integration do
end
let_it_be(:pipeline, reload: true) do
- create(:ci_pipeline, project: project,
- sha: project.commit.id,
- ref: project.default_branch)
+ create(:ci_pipeline, project: project, sha: project.commit.id, ref: project.default_branch)
end
let(:user) { create(:user) }
@@ -25,10 +23,14 @@ RSpec.describe API::Ci::Jobs, feature_category: :continuous_integration do
let(:guest) { create(:project_member, :guest, project: project).user }
let(:running_job) do
- create(:ci_build, :running, project: project,
- user: user,
- pipeline: pipeline,
- artifacts_expire_at: 1.day.since)
+ create(
+ :ci_build,
+ :running,
+ project: project,
+ user: user,
+ pipeline: pipeline,
+ artifacts_expire_at: 1.day.since
+ )
end
let!(:job) do
@@ -266,7 +268,7 @@ RSpec.describe API::Ci::Jobs, feature_category: :continuous_integration do
expect(json_response.dig('project', 'groups')).to match_array([{ 'id' => group.id }])
expect(json_response.dig('user', 'id')).to eq(api_user.id)
expect(json_response.dig('user', 'username')).to eq(api_user.username)
- expect(json_response.dig('user', 'roles_in_project')).to match_array %w(guest reporter developer)
+ expect(json_response.dig('user', 'roles_in_project')).to match_array %w[guest reporter developer]
expect(json_response).not_to include('environment')
end
@@ -450,7 +452,7 @@ RSpec.describe API::Ci::Jobs, feature_category: :continuous_integration do
end
context 'filter project with array of scope elements' do
- let(:query) { { scope: %w(pending running) } }
+ let(:query) { { scope: %w[pending running] } }
it do
expect(response).to have_gitlab_http_status(:ok)
@@ -459,7 +461,7 @@ RSpec.describe API::Ci::Jobs, feature_category: :continuous_integration do
end
context 'respond 400 when scope contains invalid state' do
- let(:query) { { scope: %w(unknown running) } }
+ let(:query) { { scope: %w[unknown running] } }
it { expect(response).to have_gitlab_http_status(:bad_request) }
end
@@ -789,14 +791,14 @@ RSpec.describe API::Ci::Jobs, feature_category: :continuous_integration do
end
context 'authorized user' do
- context 'user with :update_build persmission' do
+ context 'user with :cancel_build permission' do
it 'cancels running or pending job' do
expect(response).to have_gitlab_http_status(:created)
expect(project.builds.first.status).to eq('success')
end
end
- context 'user without :update_build permission' do
+ context 'user without :cancel_build permission' do
let(:api_user) { reporter }
it 'does not cancel job' do
diff --git a/spec/requests/api/ci/pipelines_spec.rb b/spec/requests/api/ci/pipelines_spec.rb
index 3544a6dd72a..eef125e1bc3 100644
--- a/spec/requests/api/ci/pipelines_spec.rb
+++ b/spec/requests/api/ci/pipelines_spec.rb
@@ -13,8 +13,14 @@ RSpec.describe API::Ci::Pipelines, feature_category: :continuous_integration do
end
let_it_be(:pipeline) do
- create(:ci_empty_pipeline, project: project, sha: project.commit.id,
- ref: project.default_branch, user: user, name: 'Build pipeline')
+ create(
+ :ci_empty_pipeline,
+ project: project,
+ sha: project.commit.id,
+ ref: project.default_branch,
+ user: user,
+ name: 'Build pipeline'
+ )
end
before do
@@ -357,8 +363,13 @@ RSpec.describe API::Ci::Pipelines, feature_category: :continuous_integration do
let(:query) { {} }
let(:api_user) { user }
let_it_be(:job) do
- create(:ci_build, :success, name: 'build', pipeline: pipeline,
- artifacts_expire_at: 1.day.since)
+ create(
+ :ci_build,
+ :success,
+ name: 'build',
+ pipeline: pipeline,
+ artifacts_expire_at: 1.day.since
+ )
end
let(:guest) { create(:project_member, :guest, project: project).user }
@@ -436,7 +447,7 @@ RSpec.describe API::Ci::Pipelines, feature_category: :continuous_integration do
end
context 'filter jobs with array of scope elements' do
- let(:query) { { scope: %w(pending running) } }
+ let(:query) { { scope: %w[pending running] } }
it :aggregate_failures do
expect(response).to have_gitlab_http_status(:ok)
@@ -445,7 +456,7 @@ RSpec.describe API::Ci::Pipelines, feature_category: :continuous_integration do
end
context 'respond 400 when scope contains invalid state' do
- let(:query) { { scope: %w(unknown running) } }
+ let(:query) { { scope: %w[unknown running] } }
it { expect(response).to have_gitlab_http_status(:bad_request) }
end
@@ -540,12 +551,14 @@ RSpec.describe API::Ci::Pipelines, feature_category: :continuous_integration do
let(:downstream_pipeline) { create(:ci_pipeline) }
let!(:pipeline_source) do
- create(:ci_sources_pipeline,
- source_pipeline: pipeline,
- source_project: project,
- source_job: bridge,
- pipeline: downstream_pipeline,
- project: downstream_pipeline.project)
+ create(
+ :ci_sources_pipeline,
+ source_pipeline: pipeline,
+ source_project: project,
+ source_job: bridge,
+ pipeline: downstream_pipeline,
+ project: downstream_pipeline.project
+ )
end
let(:query) { {} }
@@ -615,7 +628,7 @@ RSpec.describe API::Ci::Pipelines, feature_category: :continuous_integration do
end
context 'with array of scope elements' do
- let(:query) { { scope: %w(pending running) } }
+ let(:query) { { scope: %w[pending running] } }
it :skip_before_request, :aggregate_failures do
get api("/projects/#{project.id}/pipelines/#{pipeline.id}/bridges", api_user), params: query
@@ -623,14 +636,14 @@ RSpec.describe API::Ci::Pipelines, feature_category: :continuous_integration do
expect(response).to have_gitlab_http_status(:ok)
expect(json_response).to be_an Array
expect(json_response.count).to eq 2
- json_response.each { |r| expect(%w(pending running).include?(r['status'])).to be true }
+ json_response.each { |r| expect(%w[pending running].include?(r['status'])).to be true }
end
end
end
context 'respond 400 when scope contains invalid state' do
context 'in an array' do
- let(:query) { { scope: %w(unknown running) } }
+ let(:query) { { scope: %w[unknown running] } }
it { expect(response).to have_gitlab_http_status(:bad_request) }
end
@@ -713,12 +726,14 @@ RSpec.describe API::Ci::Pipelines, feature_category: :continuous_integration do
def create_bridge(pipeline, status = :created)
create(:ci_bridge, status: status, pipeline: pipeline).tap do |bridge|
downstream_pipeline = create(:ci_pipeline)
- create(:ci_sources_pipeline,
- source_pipeline: pipeline,
- source_project: pipeline.project,
- source_job: bridge,
- pipeline: downstream_pipeline,
- project: downstream_pipeline.project)
+ create(
+ :ci_sources_pipeline,
+ source_pipeline: pipeline,
+ source_project: pipeline.project,
+ source_job: bridge,
+ pipeline: downstream_pipeline,
+ project: downstream_pipeline.project
+ )
end
end
end
@@ -914,13 +929,24 @@ RSpec.describe API::Ci::Pipelines, feature_category: :continuous_integration do
let(:second_branch) { project.repository.branches[2] }
let!(:second_pipeline) do
- create(:ci_empty_pipeline, project: project, sha: second_branch.target,
- ref: second_branch.name, user: user, name: 'Build pipeline')
+ create(
+ :ci_empty_pipeline,
+ project: project,
+ sha: second_branch.target,
+ ref: second_branch.name,
+ user: user,
+ name: 'Build pipeline'
+ )
end
before do
- create(:ci_empty_pipeline, project: project, sha: project.commit.parent.id,
- ref: project.default_branch, user: user)
+ create(
+ :ci_empty_pipeline,
+ project: project,
+ sha: project.commit.parent.id,
+ ref: project.default_branch,
+ user: user
+ )
end
context 'default repository branch' do
@@ -1107,11 +1133,82 @@ RSpec.describe API::Ci::Pipelines, feature_category: :continuous_integration do
end
end
+ describe 'PUT /projects/:id/pipelines/:pipeline_id/name' do
+ let_it_be(:pipeline_creator) { create(:user) }
+ let(:pipeline) { create(:ci_pipeline, project: project, user: pipeline_creator) }
+ let(:name) { 'A new pipeline name' }
+
+ subject(:execute) do
+ put api("/projects/#{project.id}/pipelines/#{pipeline.id}/metadata", current_user), params: { name: name }
+ end
+
+ context 'authorized user' do
+ let(:current_user) { create(:user) }
+
+ before do
+ project.add_developer(current_user)
+ end
+
+ it 'renames pipeline when name is valid', :aggregate_failures do
+ expect { execute }.to change { pipeline.reload.name }.to(name)
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+
+ context 'when name is invalid' do
+ let(:name) { 'a' * 256 }
+
+ it 'does not rename pipeline', :aggregate_failures do
+ expect { execute }.not_to change { pipeline.reload.name }
+ expect(response).to have_gitlab_http_status(:bad_request)
+ expect(json_response['message']).to eq('Failed to update pipeline - Name is too long (maximum is 255 characters)')
+ end
+ end
+ end
+
+ context 'unauthorized user' do
+ let(:current_user) { create(:user) }
+
+ context 'when user is not a member' do
+ it 'does not rename pipeline', :aggregate_failures do
+ expect { execute }.not_to change { pipeline.reload.name }
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+
+ context 'when user is a member' do
+ before do
+ project.add_reporter(current_user)
+ end
+
+ it 'does not rename pipeline', :aggregate_failures do
+ expect { execute }.not_to change { pipeline.reload.name }
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
+ end
+ end
+
+ context 'when authorized with job token' do
+ let(:job) { create(:ci_build, :running, pipeline: pipeline, project: project, user: pipeline.user) }
+
+ before do
+ project.add_developer(pipeline.user)
+ end
+
+ subject(:execute) do
+ put api("/projects/#{project.id}/pipelines/#{pipeline.id}/metadata", nil, job_token: job.token), params: { name: name }
+ end
+
+ it 'renames pipeline when name is valid', :aggregate_failures do
+ expect { execute }.to change { pipeline.reload.name }.to(name)
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+ end
+ end
+
describe 'POST /projects/:id/pipelines/:pipeline_id/retry' do
context 'authorized user' do
let_it_be(:pipeline) do
- create(:ci_pipeline, project: project, sha: project.commit.id,
- ref: project.default_branch)
+ create(:ci_pipeline, project: project, sha: project.commit.id, ref: project.default_branch)
end
let_it_be(:build) { create(:ci_build, :failed, pipeline: pipeline) }
@@ -1156,8 +1253,7 @@ RSpec.describe API::Ci::Pipelines, feature_category: :continuous_integration do
describe 'POST /projects/:id/pipelines/:pipeline_id/cancel' do
let_it_be(:pipeline) do
- create(:ci_empty_pipeline, project: project, sha: project.commit.id,
- ref: project.default_branch)
+ create(:ci_empty_pipeline, project: project, sha: project.commit.id, ref: project.default_branch)
end
let_it_be(:build) { create(:ci_build, :running, pipeline: pipeline) }
diff --git a/spec/requests/api/ci/resource_groups_spec.rb b/spec/requests/api/ci/resource_groups_spec.rb
index 26265aec1dc..809b1c7c3d3 100644
--- a/spec/requests/api/ci/resource_groups_spec.rb
+++ b/spec/requests/api/ci/resource_groups_spec.rb
@@ -126,9 +126,7 @@ RSpec.describe API::Ci::ResourceGroups, feature_category: :continuous_delivery d
context 'when resource group key contains a slash' do
let_it_be(:resource_group) { create(:ci_resource_group, project: project, key: 'test/test') }
let_it_be(:upcoming_processable) do
- create(:ci_processable,
- :waiting_for_resource,
- resource_group: resource_group)
+ create(:ci_processable, :waiting_for_resource, resource_group: resource_group)
end
let(:key) { 'test%2Ftest' }
diff --git a/spec/requests/api/ci/runner/jobs_artifacts_spec.rb b/spec/requests/api/ci/runner/jobs_artifacts_spec.rb
index 2e0be23ba90..637469411d5 100644
--- a/spec/requests/api/ci/runner/jobs_artifacts_spec.rb
+++ b/spec/requests/api/ci/runner/jobs_artifacts_spec.rb
@@ -30,8 +30,15 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state, feature_catego
describe '/api/v4/jobs' do
let(:job) do
- create(:ci_build, :artifacts, :extended_options,
- pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0)
+ create(
+ :ci_build,
+ :artifacts,
+ :extended_options,
+ pipeline: pipeline,
+ name: 'spinach',
+ stage: 'test',
+ stage_idx: 0
+ )
end
describe 'artifacts' do
diff --git a/spec/requests/api/ci/runner/jobs_request_post_spec.rb b/spec/requests/api/ci/runner/jobs_request_post_spec.rb
index 7f9c9a13311..2a870a25ea6 100644
--- a/spec/requests/api/ci/runner/jobs_request_post_spec.rb
+++ b/spec/requests/api/ci/runner/jobs_request_post_spec.rb
@@ -24,8 +24,17 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state, feature_catego
let(:runner) { create(:ci_runner, :project, projects: [project]) }
let(:pipeline) { create(:ci_pipeline, project: project, ref: 'master') }
let(:job) do
- create(:ci_build, :pending, :queued, :artifacts, :extended_options,
- pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0)
+ create(
+ :ci_build,
+ :pending,
+ :queued,
+ :artifacts,
+ :extended_options,
+ pipeline: pipeline,
+ name: 'spinach',
+ stage: 'test',
+ stage_idx: 0
+ )
end
describe 'POST /api/v4/jobs/request' do
@@ -202,12 +211,12 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state, feature_catego
let(:expected_steps) do
[{ 'name' => 'script',
- 'script' => %w(echo),
+ 'script' => %w[echo],
'timeout' => job.metadata_timeout,
'when' => 'on_success',
'allow_failure' => false },
{ 'name' => 'after_script',
- 'script' => %w(ls date),
+ 'script' => %w[ls date],
'timeout' => job.metadata_timeout,
'when' => 'always',
'allow_failure' => true }]
@@ -226,7 +235,7 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state, feature_catego
let(:expected_artifacts) do
[{ 'name' => 'artifacts_file',
'untracked' => false,
- 'paths' => %w(out/),
+ 'paths' => %w[out/],
'when' => 'always',
'expire_in' => '7d',
"artifact_type" => "archive",
@@ -342,10 +351,11 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state, feature_catego
request_job
expect(response).to have_gitlab_http_status(:created)
- expect(json_response['git_info']['refspecs'])
- .to contain_exactly("+refs/pipelines/#{pipeline.id}:refs/pipelines/#{pipeline.id}",
- '+refs/tags/*:refs/tags/*',
- '+refs/heads/*:refs/remotes/origin/*')
+ expect(json_response['git_info']['refspecs']).to contain_exactly(
+ "+refs/pipelines/#{pipeline.id}:refs/pipelines/#{pipeline.id}",
+ '+refs/tags/*:refs/tags/*',
+ '+refs/heads/*:refs/remotes/origin/*'
+ )
end
end
end
@@ -383,10 +393,11 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state, feature_catego
request_job
expect(response).to have_gitlab_http_status(:created)
- expect(json_response['git_info']['refspecs'])
- .to contain_exactly("+refs/pipelines/#{pipeline.id}:refs/pipelines/#{pipeline.id}",
- '+refs/tags/*:refs/tags/*',
- '+refs/heads/*:refs/remotes/origin/*')
+ expect(json_response['git_info']['refspecs']).to contain_exactly(
+ "+refs/pipelines/#{pipeline.id}:refs/pipelines/#{pipeline.id}",
+ '+refs/tags/*:refs/tags/*',
+ '+refs/heads/*:refs/remotes/origin/*'
+ )
end
end
end
@@ -461,7 +472,7 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state, feature_catego
expect { request_job }.to change { runner.reload.contacted_at }
end
- %w(version revision platform architecture).each do |param|
+ %w[version revision platform architecture].each do |param|
context "when info parameter '#{param}' is present" do
let(:value) { "#{param}_value" }
@@ -646,8 +657,16 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state, feature_catego
context 'when job has code coverage report' do
let(:job) do
- create(:ci_build, :pending, :queued, :coverage_report_cobertura,
- pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0)
+ create(
+ :ci_build,
+ :pending,
+ :queued,
+ :coverage_report_cobertura,
+ pipeline: pipeline,
+ name: 'spinach',
+ stage: 'test',
+ stage_idx: 0
+ )
end
let(:expected_artifacts) do
@@ -788,9 +807,16 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state, feature_catego
describe 'time_in_queue_seconds support' do
let(:job) do
- create(:ci_build, :pending, :queued, pipeline: pipeline,
- name: 'spinach', stage: 'test', stage_idx: 0,
- queued_at: 60.seconds.ago)
+ create(
+ :ci_build,
+ :pending,
+ :queued,
+ pipeline: pipeline,
+ name: 'spinach',
+ stage: 'test',
+ stage_idx: 0,
+ queued_at: 60.seconds.ago
+ )
end
it 'presents the time_in_queue_seconds info in the payload' do
diff --git a/spec/requests/api/ci/runner/jobs_trace_spec.rb b/spec/requests/api/ci/runner/jobs_trace_spec.rb
index ee00fc5a793..8c596d2338f 100644
--- a/spec/requests/api/ci/runner/jobs_trace_spec.rb
+++ b/spec/requests/api/ci/runner/jobs_trace_spec.rb
@@ -23,14 +23,28 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_trace_chunks, feature_catego
let(:runner) { create(:ci_runner, :project, projects: [project]) }
let(:user) { create(:user) }
let(:job) do
- create(:ci_build, :artifacts, :extended_options,
- pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0)
+ create(
+ :ci_build,
+ :artifacts,
+ :extended_options,
+ pipeline: pipeline,
+ name: 'spinach',
+ stage: 'test',
+ stage_idx: 0
+ )
end
describe 'PATCH /api/v4/jobs/:id/trace' do
let(:job) do
- create(:ci_build, :running, :trace_live,
- project: project, user: user, runner_id: runner.id, pipeline: pipeline)
+ create(
+ :ci_build,
+ :running,
+ :trace_live,
+ project: project,
+ user: user,
+ runner_id: runner.id,
+ pipeline: pipeline
+ )
end
let(:headers) { { API::Ci::Helpers::Runner::JOB_TOKEN_HEADER => job.token, 'Content-Type' => 'text/plain' } }
diff --git a/spec/requests/api/ci/runner/runners_post_spec.rb b/spec/requests/api/ci/runner/runners_post_spec.rb
index c5e49e9ac54..1490172d1c3 100644
--- a/spec/requests/api/ci/runner/runners_post_spec.rb
+++ b/spec/requests/api/ci/runner/runners_post_spec.rb
@@ -48,7 +48,7 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state, feature_catego
description: 'server.hostname',
maintenance_note: 'Some maintainer notes',
run_untagged: false,
- tag_list: %w(tag1 tag2),
+ tag_list: %w[tag1 tag2],
locked: true,
active: true,
access_level: 'ref_protected',
@@ -167,7 +167,7 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state, feature_catego
allow_any_instance_of(::Ci::Runner).to receive(:cache_attributes)
end
- %w(name version revision platform architecture).each do |param|
+ %w[name version revision platform architecture].each do |param|
context "when info parameter '#{param}' info is present" do
let(:value) { "#{param}_value" }
@@ -185,8 +185,8 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state, feature_catego
it "sets the runner's ip_address" do
post api('/runners'),
- params: { token: registration_token },
- headers: { 'X-Forwarded-For' => '123.111.123.111' }
+ params: { token: registration_token },
+ headers: { 'X-Forwarded-For' => '123.111.123.111' }
expect(response).to have_gitlab_http_status(:created)
expect(::Ci::Runner.last.ip_address).to eq('123.111.123.111')
diff --git a/spec/requests/api/ci/runners_spec.rb b/spec/requests/api/ci/runners_spec.rb
index 2b2d2e0def8..ba80684e89e 100644
--- a/spec/requests/api/ci/runners_spec.rb
+++ b/spec/requests/api/ci/runners_spec.rb
@@ -249,6 +249,39 @@ RSpec.describe API::Ci::Runners, :aggregate_failures, feature_category: :runner_
a_hash_including('description' => 'Runner tagged with tag1 and tag2')
]
end
+
+ context 'with ci_runner_machines' do
+ let_it_be(:version_ci_runner) { create(:ci_runner, :project, description: 'Runner with machine') }
+ let_it_be(:version_ci_runner_machine) { create(:ci_runner_machine, runner: version_ci_runner, version: '15.0.3') }
+ let_it_be(:version_16_ci_runner) { create(:ci_runner, :project, description: 'Runner with machine version 16') }
+ let_it_be(:version_16_ci_runner_machine) { create(:ci_runner_machine, runner: version_16_ci_runner, version: '16.0.1') }
+
+ it 'filters runners by version_prefix when prefix is "15.0"' do
+ get api('/runners/all?version_prefix=15.0', admin, admin_mode: true)
+
+ expect(json_response).to match_array [
+ a_hash_including('description' => 'Runner with machine', 'active' => true, 'paused' => false)
+ ]
+ end
+
+ it 'filters runners by version_prefix when prefix is "16"' do
+ get api('/runners/all?version_prefix=16', admin, admin_mode: true)
+ expect(json_response).to match_array [
+ a_hash_including('description' => 'Runner with machine version 16', 'active' => true, 'paused' => false)
+ ]
+ end
+
+ it 'filters runners by version_prefix when prefix is "25"' do
+ get api('/runners/all?version_prefix=25', admin, admin_mode: true)
+ expect(json_response).to match_array []
+ end
+
+ it 'does not filter runners by version_prefix when prefix is invalid ("V15")' do
+ get api('/runners/all?version_prefix=v15', admin, admin_mode: true)
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ end
+ end
end
context 'without admin privileges' do
@@ -467,13 +500,17 @@ RSpec.describe API::Ci::Runners, :aggregate_failures, feature_category: :runner_
active = shared_runner.active
runner_queue_value = shared_runner.ensure_runner_queue_value
- update_runner(shared_runner.id, admin, description: "#{description}_updated",
- active: !active,
- tag_list: ['ruby2.1', 'pgsql', 'mysql'],
- run_untagged: 'false',
- locked: 'true',
- access_level: 'ref_protected',
- maximum_timeout: 1234)
+ update_runner(
+ shared_runner.id,
+ admin,
+ description: "#{description}_updated",
+ active: !active,
+ tag_list: ['ruby2.1', 'pgsql', 'mysql'],
+ run_untagged: 'false',
+ locked: 'true',
+ access_level: 'ref_protected',
+ maximum_timeout: 1234
+ )
shared_runner.reload
expect(response).to have_gitlab_http_status(:ok)
diff --git a/spec/requests/api/ci/triggers_spec.rb b/spec/requests/api/ci/triggers_spec.rb
index ff54ba61309..a6e50479963 100644
--- a/spec/requests/api/ci/triggers_spec.rb
+++ b/spec/requests/api/ci/triggers_spec.rb
@@ -81,7 +81,7 @@ RSpec.describe API::Ci::Triggers, feature_category: :continuous_integration do
end
it 'validates variables needs to be a map of key-valued strings' do
- post api("/projects/#{project.id}/trigger/pipeline"), params: options.merge(variables: { 'TRIGGER_KEY' => %w(1 2) }, ref: 'master')
+ post api("/projects/#{project.id}/trigger/pipeline"), params: options.merge(variables: { 'TRIGGER_KEY' => %w[1 2] }, ref: 'master')
expect(response).to have_gitlab_http_status(:bad_request)
expect(json_response['message']).to eq('variables needs to be a map of key-valued strings')
diff --git a/spec/requests/api/commits_spec.rb b/spec/requests/api/commits_spec.rb
index c3a7dbdcdbb..6a112918288 100644
--- a/spec/requests/api/commits_spec.rb
+++ b/spec/requests/api/commits_spec.rb
@@ -2426,16 +2426,6 @@ RSpec.describe API::Commits, feature_category: :source_code_management do
expect(json_response['x509_certificate']['x509_issuer']['crl_url']).to eq(commit.signature.x509_certificate.x509_issuer.crl_url)
expect(json_response['commit_source']).to eq('gitaly')
end
-
- context 'with Rugged enabled', :enable_rugged do
- it 'returns correct JSON' do
- get api(route, current_user)
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(json_response['signature_type']).to eq('X509')
- expect(json_response['commit_source']).to eq('gitaly')
- end
- end
end
context 'with ssh signed commit' do
diff --git a/spec/requests/api/container_repositories_spec.rb b/spec/requests/api/container_repositories_spec.rb
index 605fa0d92f6..82c63362166 100644
--- a/spec/requests/api/container_repositories_spec.rb
+++ b/spec/requests/api/container_repositories_spec.rb
@@ -67,7 +67,7 @@ RSpec.describe API::ContainerRepositories, feature_category: :container_registry
let(:url) { "/registry/repositories/#{repository.id}?tags=true" }
before do
- stub_container_registry_tags(repository: repository.path, tags: %w(rootA latest), with_manifest: true)
+ stub_container_registry_tags(repository: repository.path, tags: %w[rootA latest], with_manifest: true)
end
it 'returns a repository and its tags' do
@@ -102,7 +102,7 @@ RSpec.describe API::ContainerRepositories, feature_category: :container_registry
let(:url) { "/registry/repositories/#{repository.id}?tags_count=true" }
before do
- stub_container_registry_tags(repository: repository.path, tags: %w(rootA latest), with_manifest: true)
+ stub_container_registry_tags(repository: repository.path, tags: %w[rootA latest], with_manifest: true)
end
it 'returns a repository and its tags_count' do
diff --git a/spec/requests/api/deployments_spec.rb b/spec/requests/api/deployments_spec.rb
index 82ac2eed83d..41c5847e940 100644
--- a/spec/requests/api/deployments_spec.rb
+++ b/spec/requests/api/deployments_spec.rb
@@ -424,7 +424,7 @@ RSpec.describe API::Deployments, feature_category: :continuous_delivery do
)
expect(response).to have_gitlab_http_status(:bad_request)
- expect(json_response['message']['status']).to include(%{cannot transition via \"run\"})
+ expect(json_response['message']['status']).to include(%(cannot transition via \"run\"))
end
it 'links merge requests when the deployment status changes to success', :sidekiq_inline do
diff --git a/spec/requests/api/geo_spec.rb b/spec/requests/api/geo_spec.rb
index 3dec91fd2fa..c394553d14e 100644
--- a/spec/requests/api/geo_spec.rb
+++ b/spec/requests/api/geo_spec.rb
@@ -14,7 +14,7 @@ RSpec.describe API::Geo, feature_category: :geo_replication do
{
'type' => 'object',
'additionalProperties' => false,
- 'required' => %w(geo_enabled),
+ 'required' => %w[geo_enabled],
'properties' => {
'geo_enabled' => { 'type' => 'boolean' }
}
diff --git a/spec/requests/api/graphql/abuse_report_spec.rb b/spec/requests/api/graphql/abuse_report_spec.rb
index 7d0b8b35763..f74b1fb4061 100644
--- a/spec/requests/api/graphql/abuse_report_spec.rb
+++ b/spec/requests/api/graphql/abuse_report_spec.rb
@@ -2,49 +2,122 @@
require 'spec_helper'
-RSpec.describe 'abuse_report', feature_category: :insider_threat do
+RSpec.describe 'Querying an Abuse Report', feature_category: :insider_threat do
include GraphqlHelpers
let_it_be(:current_user) { create(:admin) }
- let_it_be(:label) { create(:abuse_report_label, title: 'Uno') }
- let_it_be(:report) { create(:abuse_report, labels: [label]) }
-
- let(:report_gid) { Gitlab::GlobalId.build(report, id: report.id).to_s }
-
- let(:fields) do
- <<~GRAPHQL
- labels {
- nodes {
- id
- title
- description
- color
- textColor
- }
- }
- GRAPHQL
- end
+ let_it_be(:abuse_report) { create(:abuse_report) }
- let(:arguments) { { id: report_gid } }
- let(:query) { graphql_query_for('abuseReport', arguments, fields) }
+ let(:global_id) { abuse_report.to_gid.to_s }
+ let(:abuse_report_fields) { all_graphql_fields_for('AbuseReport', max_depth: 2) }
+ let(:abuse_report_data) { graphql_data['abuseReport'] }
+
+ let(:query) do
+ graphql_query_for('abuseReport', { 'id' => global_id }, abuse_report_fields)
+ end
before do
post_graphql(query, current_user: current_user)
end
- it_behaves_like 'a working graphql query that returns data'
+ context 'when the user is an admin' do
+ it_behaves_like 'a working graphql query that returns data'
- it 'returns abuse report with labels' do
- expect(graphql_data_at('abuseReport', 'labels', 'nodes', 0)).to match(a_graphql_entity_for(label))
+ it 'returns all fields' do
+ expect(abuse_report_data).to include(
+ 'id' => global_id,
+ 'userPermissions' => {
+ 'readAbuseReport' => true,
+ 'createNote' => true
+ }
+ )
+ end
end
- context 'when current user is not an admin' do
- let_it_be(:current_user) { create(:user) }
+ context 'when the user is not an admin' do
+ let(:current_user) { create(:user) }
+
+ it 'returns nil' do
+ expect(abuse_report_data).to be_nil
+ end
+ end
- it_behaves_like 'a working graphql query'
+ describe 'labels' do
+ let_it_be(:abuse_report_label) { create(:abuse_report_label, title: 'Label') }
+ let_it_be(:abuse_report) { create(:abuse_report, labels: [abuse_report_label]) }
+
+ let(:labels_response) do
+ graphql_data_at(:abuse_report, :labels, :nodes)
+ end
+
+ let(:abuse_report_fields) do
+ <<~GRAPHQL
+ labels {
+ nodes {
+ id
+ title
+ description
+ color
+ textColor
+ }
+ }
+ GRAPHQL
+ end
+
+ it 'returns labels' do
+ expect(labels_response).to contain_exactly(
+ a_graphql_entity_for(abuse_report_label)
+ )
+ end
+ end
+
+ describe 'notes' do
+ let_it_be(:note) { create(:note, noteable: abuse_report, author: current_user) }
+
+ let(:notes_response) do
+ graphql_data_at(:abuse_report, :notes, :nodes)
+ end
+
+ let(:abuse_report_fields) do
+ <<~GRAPHQL
+ notes {
+ nodes {
+ #{all_graphql_fields_for('Note', max_depth: 2)}
+ }
+ }
+ GRAPHQL
+ end
+
+ it 'returns notes' do
+ expect(notes_response).to contain_exactly(
+ a_graphql_entity_for(note)
+ )
+ end
+ end
+
+ describe 'discussions' do
+ let_it_be(:discussion) do
+ create(:discussion_note_on_abuse_report, noteable: abuse_report, author: current_user).to_discussion
+ end
+
+ let(:discussions_response) do
+ graphql_data_at(:abuse_report, :discussions, :nodes)
+ end
+
+ let(:abuse_report_fields) do
+ <<~GRAPHQL
+ discussions {
+ nodes {
+ #{all_graphql_fields_for('Discussion', max_depth: 2)}
+ }
+ }
+ GRAPHQL
+ end
- it 'does not contain any data' do
- expect(graphql_data_at('abuseReportLabel')).to be_nil
+ it 'returns discussions' do
+ expect(discussions_response).to contain_exactly(
+ a_graphql_entity_for(discussion)
+ )
end
end
end
diff --git a/spec/requests/api/graphql/ci/catalog/resource_spec.rb b/spec/requests/api/graphql/ci/catalog/resource_spec.rb
new file mode 100644
index 00000000000..fce773f320b
--- /dev/null
+++ b/spec/requests/api/graphql/ci/catalog/resource_spec.rb
@@ -0,0 +1,341 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Query.ciCatalogResource', feature_category: :pipeline_composition do
+ include GraphqlHelpers
+
+ let_it_be(:user) { create(:user) }
+ let_it_be(:namespace) { create(:group) }
+
+ let_it_be(:project) do
+ create(
+ :project, :with_avatar, :custom_repo,
+ name: 'Component Repository',
+ description: 'A simple component',
+ namespace: namespace,
+ star_count: 1,
+ files: { 'README.md' => '[link](README.md)' }
+ )
+ end
+
+ let_it_be(:resource) { create(:ci_catalog_resource, project: project) }
+
+ let(:query) do
+ <<~GQL
+ query {
+ ciCatalogResource(id: "#{resource.to_global_id}") {
+ #{all_graphql_fields_for('CiCatalogResource', max_depth: 1)}
+ }
+ }
+ GQL
+ end
+
+ subject(:post_query) { post_graphql(query, current_user: user) }
+
+ context 'when the current user has permission to read the namespace catalog' do
+ it 'returns the resource with the expected data' do
+ namespace.add_developer(user)
+
+ post_query
+
+ expect(graphql_data_at(:ciCatalogResource)).to match(
+ a_graphql_entity_for(
+ resource, :name, :description,
+ icon: project.avatar_path,
+ webPath: "/#{project.full_path}",
+ starCount: project.star_count,
+ forksCount: project.forks_count,
+ readmeHtml: a_string_including(
+ "#{project.full_path}/-/blob/#{project.default_branch}/README.md"
+ )
+ )
+ )
+ end
+ end
+
+ context 'when the current user does not have permission to read the namespace catalog' do
+ it 'returns nil' do
+ namespace.add_guest(user)
+
+ post_query
+
+ expect(graphql_data_at(:ciCatalogResource)).to be_nil
+ end
+ end
+
+ describe 'versions' do
+ before_all do
+ namespace.add_developer(user)
+ end
+
+ before do
+ stub_licensed_features(ci_namespace_catalog: true)
+ end
+
+ let(:query) do
+ <<~GQL
+ query {
+ ciCatalogResource(id: "#{resource.to_global_id}") {
+ id
+ versions {
+ nodes {
+ id
+ tagName
+ releasedAt
+ author {
+ id
+ name
+ webUrl
+ }
+ }
+ }
+ }
+ }
+ GQL
+ end
+
+ context 'when the resource has versions' do
+ let_it_be(:author) { create(:user, name: 'author') }
+
+ let_it_be(:version1) do
+ create(:release, project: project, released_at: '2023-01-01T00:00:00Z', author: author)
+ end
+
+ let_it_be(:version2) do
+ create(:release, project: project, released_at: '2023-02-01T00:00:00Z', author: author)
+ end
+
+ it 'returns the resource with the versions data' do
+ post_query
+
+ expect(graphql_data_at(:ciCatalogResource)).to match(
+ a_graphql_entity_for(resource)
+ )
+
+ expect(graphql_data_at(:ciCatalogResource, :versions, :nodes)).to contain_exactly(
+ a_graphql_entity_for(
+ version1,
+ tagName: version1.tag,
+ releasedAt: version1.released_at,
+ author: a_graphql_entity_for(author, :name)
+ ),
+ a_graphql_entity_for(
+ version2,
+ tagName: version2.tag,
+ releasedAt: version2.released_at,
+ author: a_graphql_entity_for(author, :name)
+ )
+ )
+ end
+ end
+
+ context 'when the resource does not have a version' do
+ it 'returns versions as an empty array' do
+ post_query
+
+ expect(graphql_data_at(:ciCatalogResource)).to match(
+ a_graphql_entity_for(resource, versions: { 'nodes' => [] })
+ )
+ end
+ end
+ end
+
+ describe 'latestVersion' do
+ before_all do
+ namespace.add_developer(user)
+ end
+
+ before do
+ stub_licensed_features(ci_namespace_catalog: true)
+ end
+
+ let(:query) do
+ <<~GQL
+ query {
+ ciCatalogResource(id: "#{resource.to_global_id}") {
+ id
+ latestVersion {
+ id
+ tagName
+ releasedAt
+ author {
+ id
+ name
+ webUrl
+ }
+ }
+ }
+ }
+ GQL
+ end
+
+ context 'when the resource has versions' do
+ let_it_be(:author) { create(:user, name: 'author') }
+
+ let_it_be(:latest_version) do
+ create(:release, project: project, released_at: '2023-02-01T00:00:00Z', author: author)
+ end
+
+ before_all do
+ # Previous version of the project
+ create(:release, project: project, released_at: '2023-01-01T00:00:00Z', author: author)
+ end
+
+ it 'returns the resource with the latest version data' do
+ post_query
+
+ expect(graphql_data_at(:ciCatalogResource)).to match(
+ a_graphql_entity_for(
+ resource,
+ latestVersion: a_graphql_entity_for(
+ latest_version,
+ tagName: latest_version.tag,
+ releasedAt: latest_version.released_at,
+ author: a_graphql_entity_for(author, :name)
+ )
+ )
+ )
+ end
+ end
+
+ context 'when the resource does not have a version' do
+ it 'returns nil' do
+ post_query
+
+ expect(graphql_data_at(:ciCatalogResource)).to match(
+ a_graphql_entity_for(resource, latestVersion: nil)
+ )
+ end
+ end
+ end
+
+ describe 'rootNamespace' do
+ before_all do
+ namespace.add_developer(user)
+ end
+
+ before do
+ stub_licensed_features(ci_namespace_catalog: true)
+ end
+
+ let(:query) do
+ <<~GQL
+ query {
+ ciCatalogResource(id: "#{resource.to_global_id}") {
+ id
+ rootNamespace {
+ id
+ name
+ path
+ }
+ }
+ }
+ GQL
+ end
+
+ it 'returns the correct root namespace data' do
+ post_query
+
+ expect(graphql_data_at(:ciCatalogResource)).to match(
+ a_graphql_entity_for(
+ resource,
+ rootNamespace: a_graphql_entity_for(namespace, :name, :path)
+ )
+ )
+ end
+ end
+
+ describe 'openIssuesCount' do
+ before do
+ stub_licensed_features(ci_namespace_catalog: true)
+ end
+
+ context 'when open_issue_count is requested' do
+ let(:query) do
+ <<~GQL
+ query {
+ ciCatalogResource(id: "#{resource.to_global_id}") {
+ openIssuesCount
+ }
+ }
+ GQL
+ end
+
+ it 'returns the correct count' do
+ create(:issue, :opened, project: project)
+ create(:issue, :opened, project: project)
+
+ namespace.add_developer(user)
+
+ post_query
+
+ expect(graphql_data_at(:ciCatalogResource)).to match(
+ a_graphql_entity_for(
+ open_issues_count: 2
+ )
+ )
+ end
+
+ context 'when open_issue_count is zero' do
+ it 'returns zero' do
+ namespace.add_developer(user)
+
+ post_query
+
+ expect(graphql_data_at(:ciCatalogResource)).to match(
+ a_graphql_entity_for(
+ open_issues_count: 0
+ )
+ )
+ end
+ end
+ end
+ end
+
+ describe 'openMergeRequestsCount' do
+ before do
+ stub_licensed_features(ci_namespace_catalog: true)
+ end
+
+ context 'when merge_requests_count is requested' do
+ let(:query) do
+ <<~GQL
+ query {
+ ciCatalogResource(id: "#{resource.to_global_id}") {
+ openMergeRequestsCount
+ }
+ }
+ GQL
+ end
+
+ it 'returns the correct count' do
+ create(:merge_request, :opened, source_project: project)
+
+ namespace.add_developer(user)
+
+ post_query
+
+ expect(graphql_data_at(:ciCatalogResource)).to match(
+ a_graphql_entity_for(
+ open_merge_requests_count: 1
+ )
+ )
+ end
+
+ context 'when open merge_requests_count is zero' do
+ it 'returns zero' do
+ namespace.add_developer(user)
+
+ post_query
+
+ expect(graphql_data_at(:ciCatalogResource)).to match(
+ a_graphql_entity_for(
+ open_merge_requests_count: 0
+ )
+ )
+ end
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/ci/catalog/resources_spec.rb b/spec/requests/api/graphql/ci/catalog/resources_spec.rb
new file mode 100644
index 00000000000..7c955a1202c
--- /dev/null
+++ b/spec/requests/api/graphql/ci/catalog/resources_spec.rb
@@ -0,0 +1,359 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Query.ciCatalogResources', feature_category: :pipeline_composition do
+ include GraphqlHelpers
+
+ let_it_be(:user) { create(:user) }
+ let_it_be(:namespace) { create(:group) }
+ let_it_be(:project2) { create(:project, namespace: namespace) }
+
+ let_it_be(:project1) do
+ create(
+ :project, :with_avatar, :custom_repo,
+ name: 'Component Repository',
+ description: 'A simple component',
+ namespace: namespace,
+ star_count: 1,
+ files: { 'README.md' => '**Test**' }
+ )
+ end
+
+ let_it_be(:public_project) do
+ create(
+ :project, :with_avatar, :custom_repo, :public,
+ name: 'Public Component',
+ description: 'A public component',
+ files: { 'README.md' => '**Test**' }
+ )
+ end
+
+ let_it_be(:resource1) { create(:ci_catalog_resource, project: project1, latest_released_at: '2023-01-01T00:00:00Z') }
+ let_it_be(:public_resource) { create(:ci_catalog_resource, project: public_project) }
+
+ let(:query) do
+ <<~GQL
+ query {
+ ciCatalogResources {
+ nodes {
+ id
+ name
+ description
+ icon
+ webPath
+ latestReleasedAt
+ starCount
+ forksCount
+ readmeHtml
+ }
+ }
+ }
+ GQL
+ end
+
+ subject(:post_query) { post_graphql(query, current_user: user) }
+
+ shared_examples 'avoids N+1 queries' do
+ it do
+ ctx = { current_user: user }
+
+ control_count = ActiveRecord::QueryRecorder.new do
+ run_with_clean_state(query, context: ctx)
+ end
+
+ create(:ci_catalog_resource, project: project2)
+
+ expect do
+ run_with_clean_state(query, context: ctx)
+ end.not_to exceed_query_limit(control_count)
+ end
+ end
+
+ it_behaves_like 'avoids N+1 queries'
+
+ it 'returns the resources with the expected data' do
+ namespace.add_developer(user)
+
+ post_query
+
+ expect(graphql_data_at(:ciCatalogResources, :nodes)).to contain_exactly(
+ a_graphql_entity_for(
+ resource1, :name, :description,
+ icon: project1.avatar_path,
+ webPath: "/#{project1.full_path}",
+ starCount: project1.star_count,
+ forksCount: project1.forks_count,
+ readmeHtml: a_string_including('Test</strong>'),
+ latestReleasedAt: resource1.latest_released_at
+ ),
+ a_graphql_entity_for(public_resource)
+ )
+ end
+
+ describe 'versions' do
+ before_all do
+ namespace.add_developer(user)
+ end
+
+ let(:query) do
+ <<~GQL
+ query {
+ ciCatalogResources {
+ nodes {
+ id
+ versions {
+ nodes {
+ id
+ tagName
+ releasedAt
+ author {
+ id
+ name
+ webUrl
+ }
+ }
+ }
+ }
+ }
+ }
+ GQL
+ end
+
+ it 'limits the request to 1 resource at a time' do
+ create(:ci_catalog_resource, project: project2)
+
+ post_query
+
+ expect_graphql_errors_to_include \
+ [/"versions" field can be requested only for 1 CiCatalogResource\(s\) at a time./]
+ end
+ end
+
+ describe 'latestVersion' do
+ let_it_be(:author1) { create(:user, name: 'author1') }
+ let_it_be(:author2) { create(:user, name: 'author2') }
+
+ let_it_be(:latest_version1) do
+ create(:release, project: project1, released_at: '2023-02-01T00:00:00Z', author: author1)
+ end
+
+ let_it_be(:latest_version2) do
+ create(:release, project: public_project, released_at: '2023-02-01T00:00:00Z', author: author2)
+ end
+
+ let(:query) do
+ <<~GQL
+ query {
+ ciCatalogResources {
+ nodes {
+ id
+ latestVersion {
+ id
+ tagName
+ releasedAt
+ author {
+ id
+ name
+ webUrl
+ }
+ }
+ }
+ }
+ }
+ GQL
+ end
+
+ before_all do
+ namespace.add_developer(user)
+
+ # Previous versions of the projects
+ create(:release, project: project1, released_at: '2023-01-01T00:00:00Z', author: author1)
+ create(:release, project: public_project, released_at: '2023-01-01T00:00:00Z', author: author2)
+ end
+
+ it 'returns all resources with the latest version data' do
+ post_query
+
+ expect(graphql_data_at(:ciCatalogResources, :nodes)).to contain_exactly(
+ a_graphql_entity_for(
+ resource1,
+ latestVersion: a_graphql_entity_for(
+ latest_version1,
+ tagName: latest_version1.tag,
+ releasedAt: latest_version1.released_at,
+ author: a_graphql_entity_for(author1, :name)
+ )
+ ),
+ a_graphql_entity_for(
+ public_resource,
+ latestVersion: a_graphql_entity_for(
+ latest_version2,
+ tagName: latest_version2.tag,
+ releasedAt: latest_version2.released_at,
+ author: a_graphql_entity_for(author2, :name)
+ )
+ )
+ )
+ end
+
+ # TODO: https://gitlab.com/gitlab-org/gitlab/-/issues/430350
+ # it_behaves_like 'avoids N+1 queries'
+ end
+
+ describe 'rootNamespace' do
+ before_all do
+ namespace.add_developer(user)
+ end
+
+ let(:query) do
+ <<~GQL
+ query {
+ ciCatalogResources {
+ nodes {
+ id
+ rootNamespace {
+ id
+ name
+ path
+ }
+ }
+ }
+ }
+ GQL
+ end
+
+ it 'returns the correct root namespace data' do
+ post_query
+
+ expect(graphql_data_at(:ciCatalogResources, :nodes)).to contain_exactly(
+ a_graphql_entity_for(
+ resource1,
+ rootNamespace: a_graphql_entity_for(namespace, :name, :path)
+ ),
+ a_graphql_entity_for(public_resource, rootNamespace: nil)
+ )
+ end
+ end
+
+ describe 'openIssuesCount' do
+ before_all do
+ namespace.add_developer(user)
+ end
+
+ before_all do
+ create(:issue, :opened, project: project1)
+ create(:issue, :opened, project: project1)
+
+ create(:issue, :opened, project: public_project)
+ end
+
+ let(:query) do
+ <<~GQL
+ query {
+ ciCatalogResources {
+ nodes {
+ openIssuesCount
+ }
+ }
+ }
+ GQL
+ end
+
+ it 'returns the correct count' do
+ post_query
+
+ expect(graphql_data_at(:ciCatalogResources, :nodes)).to contain_exactly(
+ a_graphql_entity_for(openIssuesCount: 2),
+ a_graphql_entity_for(openIssuesCount: 1)
+ )
+ end
+
+ it_behaves_like 'avoids N+1 queries'
+ end
+
+ describe 'openMergeRequestsCount' do
+ before_all do
+ namespace.add_developer(user)
+ end
+
+ before_all do
+ create(:merge_request, :opened, source_project: project1)
+ create(:merge_request, :opened, source_project: public_project)
+ end
+
+ let(:query) do
+ <<~GQL
+ query {
+ ciCatalogResources {
+ nodes {
+ openMergeRequestsCount
+ }
+ }
+ }
+ GQL
+ end
+
+ it 'returns the correct count' do
+ post_query
+
+ expect(graphql_data_at(:ciCatalogResources, :nodes)).to contain_exactly(
+ a_graphql_entity_for(openMergeRequestsCount: 1),
+ a_graphql_entity_for(openMergeRequestsCount: 1)
+ )
+ end
+
+ it_behaves_like 'avoids N+1 queries'
+ end
+
+ # TODO: https://gitlab.com/gitlab-org/gitlab/-/issues/429636
+ context 'when using `projectPath` (legacy) to fetch resources' do
+ let(:query) do
+ <<~GQL
+ query {
+ ciCatalogResources(projectPath: "#{project1.full_path}") {
+ nodes {
+ #{all_graphql_fields_for('CiCatalogResource', max_depth: 1)}
+ }
+ }
+ }
+ GQL
+ end
+
+ context 'when the current user has permission to read the namespace catalog' do
+ before_all do
+ namespace.add_developer(user)
+ end
+
+ it 'returns catalog resources with the expected data' do
+ resource2 = create(:ci_catalog_resource, project: project2)
+ _resource_in_another_namespace = create(:ci_catalog_resource)
+
+ post_query
+
+ expect(graphql_data_at(:ciCatalogResources, :nodes)).to contain_exactly(
+ a_graphql_entity_for(resource1),
+ a_graphql_entity_for(
+ resource2, :name, :description,
+ icon: project2.avatar_path,
+ webPath: "/#{project2.full_path}",
+ starCount: project2.star_count,
+ forksCount: project2.forks_count,
+ readmeHtml: '',
+ latestReleasedAt: resource2.latest_released_at
+ )
+ )
+ end
+
+ it_behaves_like 'avoids N+1 queries'
+ end
+
+ context 'when the current user does not have permission to read the namespace catalog' do
+ it 'returns no resources' do
+ post_query
+
+ expect(graphql_data_at(:ciCatalogResources, :nodes)).to be_empty
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/ci/manual_variables_spec.rb b/spec/requests/api/graphql/ci/manual_variables_spec.rb
index 47dccc0deb6..41788881e62 100644
--- a/spec/requests/api/graphql/ci/manual_variables_spec.rb
+++ b/spec/requests/api/graphql/ci/manual_variables_spec.rb
@@ -90,6 +90,6 @@ RSpec.describe 'Query.project(fullPath).pipelines.jobs.manualVariables', feature
variables_data = graphql_data.dig('project', 'pipelines', 'nodes').first
.dig('jobs', 'nodes').flat_map { |job| job.dig('manualVariables', 'nodes') }
- expect(variables_data.map { |var| var['key'] }).to match_array(%w(MANUAL_TEST_VAR_1 MANUAL_TEST_VAR_2))
+ expect(variables_data.map { |var| var['key'] }).to match_array(%w[MANUAL_TEST_VAR_1 MANUAL_TEST_VAR_2])
end
end
diff --git a/spec/requests/api/graphql/ci/runners_spec.rb b/spec/requests/api/graphql/ci/runners_spec.rb
index c5571086700..0e2712d742d 100644
--- a/spec/requests/api/graphql/ci/runners_spec.rb
+++ b/spec/requests/api/graphql/ci/runners_spec.rb
@@ -35,28 +35,16 @@ RSpec.describe 'Query.runners', feature_category: :runner_fleet do
end
context 'with filters' do
- let(:query) do
- %(
- query {
- runners(type: #{runner_type}, status: #{status}) {
- #{fields}
- }
- }
- )
- end
-
- before do
- allow_next_instance_of(::Gitlab::Ci::RunnerUpgradeCheck) do |instance|
- allow(instance).to receive(:check_runner_upgrade_suggestion)
- end
-
- post_graphql(query, current_user: current_user)
- end
-
shared_examples 'a working graphql query returning expected runner' do
- it_behaves_like 'a working graphql query'
+ it_behaves_like 'a working graphql query' do
+ before do
+ post_graphql(query, current_user: current_user)
+ end
+ end
it 'returns expected runner' do
+ post_graphql(query, current_user: current_user)
+
expect(runners_graphql_data['nodes']).to contain_exactly(a_graphql_entity_for(expected_runner))
end
@@ -86,22 +74,63 @@ RSpec.describe 'Query.runners', feature_category: :runner_fleet do
end
end
- context 'runner_type is INSTANCE_TYPE and status is ACTIVE' do
- let(:runner_type) { 'INSTANCE_TYPE' }
- let(:status) { 'ACTIVE' }
+ context 'when filtered on type and status' do
+ let(:query) do
+ %(
+ query {
+ runners(type: #{runner_type}, status: #{status}) {
+ #{fields}
+ }
+ }
+ )
+ end
- let!(:expected_runner) { instance_runner }
+ before do
+ allow_next_instance_of(::Gitlab::Ci::RunnerUpgradeCheck) do |instance|
+ allow(instance).to receive(:check_runner_upgrade_suggestion)
+ end
+ end
- it_behaves_like 'a working graphql query returning expected runner'
+ context 'runner_type is INSTANCE_TYPE and status is ACTIVE' do
+ let(:runner_type) { 'INSTANCE_TYPE' }
+ let(:status) { 'ACTIVE' }
+
+ let!(:expected_runner) { instance_runner }
+
+ it_behaves_like 'a working graphql query returning expected runner'
+ end
+
+ context 'runner_type is PROJECT_TYPE and status is NEVER_CONTACTED' do
+ let(:runner_type) { 'PROJECT_TYPE' }
+ let(:status) { 'NEVER_CONTACTED' }
+
+ let!(:expected_runner) { project_runner }
+
+ it_behaves_like 'a working graphql query returning expected runner'
+ end
end
- context 'runner_type is PROJECT_TYPE and status is NEVER_CONTACTED' do
- let(:runner_type) { 'PROJECT_TYPE' }
- let(:status) { 'NEVER_CONTACTED' }
+ context 'when filtered on version prefix' do
+ let_it_be(:version_runner) { create(:ci_runner, :project, active: false, description: 'Runner with machine') }
+ let_it_be(:version_runner_machine) { create(:ci_runner_machine, runner: version_runner, version: '15.11.0') }
+
+ let(:query) do
+ %(
+ query {
+ runners(versionPrefix: "#{version_prefix}") {
+ #{fields}
+ }
+ }
+ )
+ end
+
+ context 'version_prefix is "15."' do
+ let(:version_prefix) { '15.' }
- let!(:expected_runner) { project_runner }
+ let!(:expected_runner) { version_runner }
- it_behaves_like 'a working graphql query returning expected runner'
+ it_behaves_like 'a working graphql query returning expected runner'
+ end
end
end
diff --git a/spec/requests/api/graphql/container_repository/container_repository_details_spec.rb b/spec/requests/api/graphql/container_repository/container_repository_details_spec.rb
index 118a11851dd..20277c7e27b 100644
--- a/spec/requests/api/graphql/container_repository/container_repository_details_spec.rb
+++ b/spec/requests/api/graphql/container_repository/container_repository_details_spec.rb
@@ -313,4 +313,124 @@ RSpec.describe 'container repository details', feature_category: :container_regi
end
it_behaves_like 'handling graphql network errors with the container registry'
+
+ context 'when list tags API is enabled', :saas do
+ before do
+ stub_container_registry_config(enabled: true)
+ allow(ContainerRegistry::GitlabApiClient).to receive(:supports_gitlab_api?).and_return(true)
+
+ allow_next_instances_of(ContainerRegistry::GitlabApiClient, nil) do |client|
+ allow(client).to receive(:tags).and_return(response_body)
+ end
+ end
+
+ let_it_be(:raw_tags_response) do
+ [
+ {
+ name: 'latest',
+ digest: 'sha256:1234567892',
+ config_digest: 'sha256:3332132331',
+ media_type: 'application/vnd.oci.image.manifest.v1+json',
+ size_bytes: 1234567892,
+ created_at: 10.minutes.ago,
+ updated_at: 10.minutes.ago
+ }
+ ]
+ end
+
+ let_it_be(:url) { URI('/gitlab/v1/repositories/group1/proj1/tags/list/?before=tag1') }
+
+ let_it_be(:response_body) do
+ {
+ pagination: { previous: { uri: url }, next: { uri: url } },
+ response_body: ::Gitlab::Json.parse(raw_tags_response.to_json)
+ }
+ end
+
+ it_behaves_like 'a working graphql query' do # OK
+ before do
+ subject
+ end
+
+ it 'matches the JSON schema' do
+ expect(container_repository_details_response).to match_schema('graphql/container_repository_details')
+ end
+ end
+
+ context 'with different permissions' do # OK
+ let_it_be(:user) { create(:user) }
+
+ let(:tags_response) { container_repository_details_response.dig('tags', 'nodes') }
+
+ where(:project_visibility, :role, :access_granted, :can_delete) do
+ :private | :maintainer | true | true
+ :private | :developer | true | true
+ :private | :reporter | true | false
+ :private | :guest | false | false
+ :private | :anonymous | false | false
+ :public | :maintainer | true | true
+ :public | :developer | true | true
+ :public | :reporter | true | false
+ :public | :guest | true | false
+ :public | :anonymous | true | false
+ end
+
+ with_them do
+ before do
+ project.update!(visibility_level: Gitlab::VisibilityLevel.const_get(project_visibility.to_s.upcase, false))
+ project.add_member(user, role) unless role == :anonymous
+ end
+
+ it 'return the proper response' do
+ subject
+
+ if access_granted
+ expect(tags_response.size).to eq(raw_tags_response.size)
+ expect(container_repository_details_response.dig('canDelete')).to eq(can_delete)
+ else
+ expect(container_repository_details_response).to eq(nil)
+ end
+ end
+ end
+ end
+
+ context 'querying' do
+ let(:name) { 'l' }
+ let(:tags_response) { container_repository_details_response.dig('tags', 'edges') }
+ let(:variables) do
+ { id: container_repository_global_id, n: name }
+ end
+
+ let(:query) do
+ <<~GQL
+ query($id: ContainerRepositoryID!, $n: String) {
+ containerRepository(id: $id) {
+ tags(name: $n) {
+ edges {
+ node {
+ #{all_graphql_fields_for('ContainerRepositoryTag')}
+ }
+ }
+ }
+ }
+ }
+ GQL
+ end
+
+ it 'returns the tag response', :aggregate_failures do
+ subject
+
+ expect(tags_response.size).to eq(1)
+ expect(tags_response.first.dig('node', 'name')).to eq('latest')
+ end
+
+ context 'invalid filter' do
+ let(:name) { 1 }
+
+ it_behaves_like 'returning an invalid value error'
+ end
+ end
+
+ it_behaves_like 'handling graphql network errors with the container registry'
+ end
end
diff --git a/spec/requests/api/graphql/gitlab_schema_spec.rb b/spec/requests/api/graphql/gitlab_schema_spec.rb
index d55a70f503c..060a1b42cb6 100644
--- a/spec/requests/api/graphql/gitlab_schema_spec.rb
+++ b/spec/requests/api/graphql/gitlab_schema_spec.rb
@@ -113,7 +113,7 @@ RSpec.describe 'GitlabSchema configurations', feature_category: :integrations do
context 'regular queries' do
subject do
- query = graphql_query_for('project', { 'fullPath' => project.full_path }, %w(id name description))
+ query = graphql_query_for('project', { 'fullPath' => project.full_path }, %w[id name description])
post_graphql(query)
end
@@ -125,7 +125,7 @@ RSpec.describe 'GitlabSchema configurations', feature_category: :integrations do
subject do
queries = [
- { query: graphql_query_for('project', { 'fullPath' => '$fullPath' }, %w(id name description)) }, # Complexity 4
+ { query: graphql_query_for('project', { 'fullPath' => '$fullPath' }, %w[id name description]) }, # Complexity 4
{ query: graphql_query_for('echo', { 'text' => "$test" }, []), variables: { "test" => "Hello world" } }, # Complexity 1
{ query: graphql_query_for('project', { 'fullPath' => project.full_path }, "userPermissions { createIssue }") } # Complexity 3
]
@@ -215,7 +215,7 @@ RSpec.describe 'GitlabSchema configurations', feature_category: :integrations do
context "global id's" do
it 'uses GlobalID to expose ids' do
- post_graphql(graphql_query_for('project', { 'fullPath' => project.full_path }, %w(id)),
+ post_graphql(graphql_query_for('project', { 'fullPath' => project.full_path }, %w[id]),
current_user: project.first_owner)
parsed_id = GlobalID.parse(graphql_data['project']['id'])
diff --git a/spec/requests/api/graphql/group/container_repositories_spec.rb b/spec/requests/api/graphql/group/container_repositories_spec.rb
index 51d12261247..9206ead1534 100644
--- a/spec/requests/api/graphql/group/container_repositories_spec.rb
+++ b/spec/requests/api/graphql/group/container_repositories_spec.rb
@@ -49,7 +49,7 @@ RSpec.describe 'getting container repositories in a group', feature_category: :s
group.add_owner(owner)
stub_container_registry_config(enabled: true)
container_repositories.each do |repository|
- stub_container_registry_tags(repository: repository.path, tags: %w(tag1 tag2 tag3), with_manifest: false)
+ stub_container_registry_tags(repository: repository.path, tags: %w[tag1 tag2 tag3], with_manifest: false)
end
end
@@ -142,7 +142,7 @@ RSpec.describe 'getting container repositories in a group', feature_category: :s
end
before do
- stub_container_registry_tags(repository: container_repository.path, tags: %w(tag4 tag5 tag6), with_manifest: false)
+ stub_container_registry_tags(repository: container_repository.path, tags: %w[tag4 tag5 tag6], with_manifest: false)
end
it 'returns the searched container repository' do
diff --git a/spec/requests/api/graphql/group/data_transfer_spec.rb b/spec/requests/api/graphql/group/data_transfer_spec.rb
index b7c038afa54..e17074a0247 100644
--- a/spec/requests/api/graphql/group/data_transfer_spec.rb
+++ b/spec/requests/api/graphql/group/data_transfer_spec.rb
@@ -71,45 +71,21 @@ RSpec.describe 'group data transfers', feature_category: :source_code_management
context 'when user has enough permissions' do
before do
group.add_owner(current_user)
+ subject
end
- context 'when data_transfer_monitoring_mock_data is NOT enabled' do
- before do
- stub_feature_flags(data_transfer_monitoring_mock_data: false)
- subject
- end
-
- it 'returns real results' do
- expect(response).to have_gitlab_http_status(:ok)
+ it 'returns real results' do
+ expect(response).to have_gitlab_http_status(:ok)
- expect(egress_data.count).to eq(2)
+ expect(egress_data.count).to eq(2)
- expect(egress_data.first.keys).to match_array(
- %w[date totalEgress repositoryEgress artifactsEgress packagesEgress registryEgress]
- )
+ expect(egress_data.first.keys).to match_array(
+ %w[date totalEgress repositoryEgress artifactsEgress packagesEgress registryEgress]
+ )
- expect(egress_data.pluck('repositoryEgress')).to match_array(%w[1 6])
- end
-
- it_behaves_like 'a working graphql query'
+ expect(egress_data.pluck('repositoryEgress')).to match_array(%w[1 6])
end
- context 'when data_transfer_monitoring_mock_data is enabled' do
- before do
- stub_feature_flags(data_transfer_monitoring_mock_data: true)
- subject
- end
-
- it 'returns mock results' do
- expect(response).to have_gitlab_http_status(:ok)
-
- expect(egress_data.count).to eq(12)
- expect(egress_data.first.keys).to match_array(
- %w[date totalEgress repositoryEgress artifactsEgress packagesEgress registryEgress]
- )
- end
-
- it_behaves_like 'a working graphql query'
- end
+ it_behaves_like 'a working graphql query'
end
end
diff --git a/spec/requests/api/graphql/group/milestones_spec.rb b/spec/requests/api/graphql/group/milestones_spec.rb
index 209588835f2..6063e6d5293 100644
--- a/spec/requests/api/graphql/group/milestones_spec.rb
+++ b/spec/requests/api/graphql/group/milestones_spec.rb
@@ -136,7 +136,7 @@ RSpec.describe 'Milestones through GroupQuery', feature_category: :team_planning
let_it_be(:closed_issue) { create(:issue, :closed, project: project, milestone: milestone) }
let(:milestone_query) do
- %{
+ %(
id
title
description
@@ -149,7 +149,7 @@ RSpec.describe 'Milestones through GroupQuery', feature_category: :team_planning
projectMilestone
groupMilestone
subgroupMilestone
- }
+ )
end
def post_query
@@ -180,12 +180,12 @@ RSpec.describe 'Milestones through GroupQuery', feature_category: :team_planning
context 'milestone statistics' do
let(:milestone_query) do
- %{
+ %(
stats {
totalIssuesCount
closedIssuesCount
}
- }
+ )
end
it 'returns the correct milestone statistics' do
diff --git a/spec/requests/api/graphql/merge_requests/codequality_reports_comparer_spec.rb b/spec/requests/api/graphql/merge_requests/codequality_reports_comparer_spec.rb
index 2939e9307e9..09a229c2098 100644
--- a/spec/requests/api/graphql/merge_requests/codequality_reports_comparer_spec.rb
+++ b/spec/requests/api/graphql/merge_requests/codequality_reports_comparer_spec.rb
@@ -54,6 +54,7 @@ RSpec.describe 'Query.project.mergeRequest.codequalityReportsComparer', feature_
let(:codequality_reports_comparer_fields) do
<<~QUERY
codequalityReportsComparer {
+ status
report {
status
newErrors {
@@ -138,6 +139,7 @@ RSpec.describe 'Query.project.mergeRequest.codequalityReportsComparer', feature_
expect(result).to match(
a_hash_including(
{
+ status: 'PARSED',
report: {
status: 'FAILED',
newErrors: [
diff --git a/spec/requests/api/graphql/mutations/admin/sidekiq_queues/delete_jobs_spec.rb b/spec/requests/api/graphql/mutations/admin/sidekiq_queues/delete_jobs_spec.rb
index e3a7442ffe6..316b0f3755d 100644
--- a/spec/requests/api/graphql/mutations/admin/sidekiq_queues/delete_jobs_spec.rb
+++ b/spec/requests/api/graphql/mutations/admin/sidekiq_queues/delete_jobs_spec.rb
@@ -20,7 +20,7 @@ RSpec.describe 'Deleting Sidekiq jobs', :clean_gitlab_redis_queues, feature_cate
let(:current_user) { create(:user) }
it_behaves_like 'a mutation that returns top-level errors',
- errors: ['You must be an admin to use this mutation']
+ errors: ['You must be an admin to use this mutation']
end
context 'when the user is an admin' do
@@ -43,7 +43,7 @@ RSpec.describe 'Deleting Sidekiq jobs', :clean_gitlab_redis_queues, feature_cate
raise 'Not enqueued!' if Sidekiq::Queue.new(queue).size.zero?
end
- it 'returns info about the deleted jobs', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/425824' do
+ it 'returns info about the deleted jobs' do
add_job(admin, [1])
add_job(admin, [2])
add_job(create(:user), [3])
@@ -51,9 +51,7 @@ RSpec.describe 'Deleting Sidekiq jobs', :clean_gitlab_redis_queues, feature_cate
post_graphql_mutation(mutation, current_user: admin)
expect(mutation_response['errors']).to be_empty
- expect(mutation_response['result']).to eq('completed' => true,
- 'deletedJobs' => 2,
- 'queueSize' => 1)
+ expect(mutation_response['result']).to eq('completed' => true, 'deletedJobs' => 2, 'queueSize' => 1)
end
end
@@ -61,14 +59,14 @@ RSpec.describe 'Deleting Sidekiq jobs', :clean_gitlab_redis_queues, feature_cate
let(:variables) { { queue_name: queue } }
it_behaves_like 'a mutation that returns errors in the response',
- errors: ['No metadata provided']
+ errors: ['No metadata provided']
end
context 'when the queue does not exist' do
let(:variables) { { user: admin.username, queue_name: 'authorized_projects_2' } }
it_behaves_like 'a mutation that returns top-level errors',
- errors: ['Queue authorized_projects_2 not found']
+ errors: ['Queue authorized_projects_2 not found']
end
end
end
diff --git a/spec/requests/api/graphql/mutations/alert_management/alerts/create_alert_issue_spec.rb b/spec/requests/api/graphql/mutations/alert_management/alerts/create_alert_issue_spec.rb
index fbe6d95dfff..f2b516783e5 100644
--- a/spec/requests/api/graphql/mutations/alert_management/alerts/create_alert_issue_spec.rb
+++ b/spec/requests/api/graphql/mutations/alert_management/alerts/create_alert_issue_spec.rb
@@ -14,21 +14,23 @@ RSpec.describe 'Create an alert issue from an alert', feature_category: :inciden
project_path: project.full_path,
iid: alert.iid.to_s
}
- graphql_mutation(:create_alert_issue, variables,
- <<~QL
- clientMutationId
- errors
- alert {
- iid
- issue {
- iid
- }
- }
- issue {
- iid
- title
- }
- QL
+ graphql_mutation(
+ :create_alert_issue,
+ variables,
+ <<~QL
+ clientMutationId
+ errors
+ alert {
+ iid
+ issue {
+ iid
+ }
+ }
+ issue {
+ iid
+ title
+ }
+ QL
)
end
diff --git a/spec/requests/api/graphql/mutations/award_emojis/toggle_spec.rb b/spec/requests/api/graphql/mutations/award_emojis/toggle_spec.rb
index 65a5fb87f9a..e7e23304d81 100644
--- a/spec/requests/api/graphql/mutations/award_emojis/toggle_spec.rb
+++ b/spec/requests/api/graphql/mutations/award_emojis/toggle_spec.rb
@@ -6,9 +6,11 @@ RSpec.describe 'Toggling an AwardEmoji', feature_category: :shared do
include GraphqlHelpers
let_it_be(:current_user) { create(:user) }
- let_it_be(:project, reload: true) { create(:project) }
- let_it_be(:awardable) { create(:note, project: project) }
+ let_it_be(:group) { create(:group) }
+ let_it_be(:project, reload: true) { create(:project, group: group) }
+ let_it_be(:issue_note) { create(:note, project: project) }
+ let(:awardable) { issue_note }
let(:emoji_name) { 'thumbsup' }
let(:mutation) do
variables = {
@@ -36,8 +38,8 @@ RSpec.describe 'Toggling an AwardEmoji', feature_category: :shared do
end
context 'when the user has permission' do
- before do
- project.add_developer(current_user)
+ before_all do
+ group.add_developer(current_user)
end
context 'when the given awardable is not an Awardable' do
@@ -60,6 +62,33 @@ RSpec.describe 'Toggling an AwardEmoji', feature_category: :shared do
end
context 'when the given awardable is an Awardable' do
+ context 'when the awardable is a work item' do
+ context 'when the work item is associated directly with a group' do
+ let_it_be(:group_work_item) { create(:work_item, :group_level, namespace: group) }
+ let(:awardable) { group_work_item }
+
+ context 'when no emoji has been awarded by the current_user yet' do
+ it 'creates an emoji' do
+ expect do
+ post_graphql_mutation(mutation, current_user: current_user)
+ end.to change { AwardEmoji.count }.by(1)
+ end
+ end
+
+ context 'when an emoji has been awarded by the current_user' do
+ before do
+ create_award_emoji(current_user)
+ end
+
+ it 'removes the emoji' do
+ expect do
+ post_graphql_mutation(mutation, current_user: current_user)
+ end.to change { AwardEmoji.count }.by(-1)
+ end
+ end
+ end
+ end
+
context 'when no emoji has been awarded by the current_user yet' do
# Create an award emoji for another user. This therefore tests that
# toggling is correctly scoped to the user's emoji only.
diff --git a/spec/requests/api/graphql/mutations/boards/issues/issue_move_list_spec.rb b/spec/requests/api/graphql/mutations/boards/issues/issue_move_list_spec.rb
index df64caa1cfb..8e71d77f7bc 100644
--- a/spec/requests/api/graphql/mutations/boards/issues/issue_move_list_spec.rb
+++ b/spec/requests/api/graphql/mutations/boards/issues/issue_move_list_spec.rb
@@ -131,22 +131,24 @@ RSpec.describe 'Reposition and move issue within board lists', feature_category:
end
def mutation(additional_params = {})
- graphql_mutation(mutation_name, issue_move_params.merge(additional_params),
- <<-QL.strip_heredoc
- clientMutationId
- issue {
- iid,
- relativePosition
- labels {
- edges {
- node{
- title
- }
- }
- }
- }
- errors
- QL
+ graphql_mutation(
+ mutation_name,
+ issue_move_params.merge(additional_params),
+ <<-QL.strip_heredoc
+ clientMutationId
+ issue {
+ iid,
+ relativePosition
+ labels {
+ edges {
+ node{
+ title
+ }
+ }
+ }
+ }
+ errors
+ QL
)
end
end
diff --git a/spec/requests/api/graphql/mutations/ci/catalog/resources/create_spec.rb b/spec/requests/api/graphql/mutations/ci/catalog/resources/create_spec.rb
new file mode 100644
index 00000000000..f990cab55f4
--- /dev/null
+++ b/spec/requests/api/graphql/mutations/ci/catalog/resources/create_spec.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'CatalogResourcesCreate', feature_category: :pipeline_composition do
+ include GraphqlHelpers
+
+ let_it_be(:current_user) { create(:user) }
+ let_it_be(:project) { create(:project, :catalog_resource_with_components) }
+
+ let(:mutation) do
+ variables = {
+ project_path: project.full_path
+ }
+ graphql_mutation(:catalog_resources_create, variables,
+ <<-QL.strip_heredoc
+ errors
+ QL
+ )
+ end
+
+ context 'when unauthorized' do
+ it_behaves_like 'a mutation that returns a top-level access error'
+ end
+
+ context 'when authorized' do
+ context 'with a valid project' do
+ before_all do
+ project.add_owner(current_user)
+ end
+
+ it 'creates a catalog resource' do
+ post_graphql_mutation(mutation, current_user: current_user)
+
+ expect(graphql_mutation_response(:catalog_resources_create)['errors']).to be_empty
+ expect(response).to have_gitlab_http_status(:success)
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/mutations/ci/catalog/unpublish_spec.rb b/spec/requests/api/graphql/mutations/ci/catalog/unpublish_spec.rb
new file mode 100644
index 00000000000..07465777263
--- /dev/null
+++ b/spec/requests/api/graphql/mutations/ci/catalog/unpublish_spec.rb
@@ -0,0 +1,52 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'CatalogResourceUnpublish', feature_category: :pipeline_composition do
+ include GraphqlHelpers
+
+ let_it_be(:current_user) { create(:user) }
+ let_it_be_with_reload(:resource) { create(:ci_catalog_resource) }
+
+ let(:mutation) do
+ graphql_mutation(
+ :catalog_resource_unpublish,
+ id: resource.to_gid.to_s
+ )
+ end
+
+ subject(:post_query) { post_graphql_mutation(mutation, current_user: current_user) }
+
+ context 'when unauthorized' do
+ it_behaves_like 'a mutation that returns a top-level access error'
+ end
+
+ context 'when authorized' do
+ before_all do
+ resource.project.add_owner(current_user)
+ end
+
+ context 'when the catalog resource is in published state' do
+ it 'updates the state to draft' do
+ resource.update!(state: :published)
+ expect(resource.state).to eq('published')
+
+ post_query
+
+ expect(resource.reload.state).to eq('draft')
+ expect_graphql_errors_to_be_empty
+ end
+ end
+
+ context 'when the catalog resource is already in draft state' do
+ it 'leaves the state as draft' do
+ expect(resource.state).to eq('draft')
+
+ post_query
+
+ expect(resource.reload.state).to eq('draft')
+ expect_graphql_errors_to_be_empty
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/mutations/ci/pipeline_retry_spec.rb b/spec/requests/api/graphql/mutations/ci/pipeline_retry_spec.rb
index e7edc86bea0..70b154946ef 100644
--- a/spec/requests/api/graphql/mutations/ci/pipeline_retry_spec.rb
+++ b/spec/requests/api/graphql/mutations/ci/pipeline_retry_spec.rb
@@ -13,13 +13,15 @@ RSpec.describe 'PipelineRetry', feature_category: :continuous_integration do
variables = {
id: pipeline.to_global_id.to_s
}
- graphql_mutation(:pipeline_retry, variables,
- <<-QL
- errors
- pipeline {
- id
- }
- QL
+ graphql_mutation(
+ :pipeline_retry,
+ variables,
+ <<-QL
+ errors
+ pipeline {
+ id
+ }
+ QL
)
end
diff --git a/spec/requests/api/graphql/mutations/clusters/agent_tokens/agent_tokens/create_spec.rb b/spec/requests/api/graphql/mutations/clusters/agent_tokens/agent_tokens/create_spec.rb
index ef0d44395bf..dd4b015409b 100644
--- a/spec/requests/api/graphql/mutations/clusters/agent_tokens/agent_tokens/create_spec.rb
+++ b/spec/requests/api/graphql/mutations/clusters/agent_tokens/agent_tokens/create_spec.rb
@@ -23,8 +23,8 @@ RSpec.describe 'Create a new cluster agent token', feature_category: :deployment
context 'without user permissions' do
it_behaves_like 'a mutation that returns top-level errors',
- errors: ["The resource that you are attempting to access does not exist "\
- "or you don't have permission to perform this action"]
+ errors: ["The resource that you are attempting to access does not exist "\
+ "or you don't have permission to perform this action"]
it 'does not create a token' do
expect { post_graphql_mutation(mutation, current_user: current_user) }.not_to change(Clusters::AgentToken, :count)
diff --git a/spec/requests/api/graphql/mutations/clusters/agents/delete_spec.rb b/spec/requests/api/graphql/mutations/clusters/agents/delete_spec.rb
index b70a6282a7a..a2a093d63e6 100644
--- a/spec/requests/api/graphql/mutations/clusters/agents/delete_spec.rb
+++ b/spec/requests/api/graphql/mutations/clusters/agents/delete_spec.rb
@@ -22,8 +22,8 @@ RSpec.describe 'Delete a cluster agent', feature_category: :deployment_managemen
context 'without project permissions' do
it_behaves_like 'a mutation that returns top-level errors',
- errors: ['The resource that you are attempting to access does not exist '\
- 'or you don\'t have permission to perform this action']
+ errors: ['The resource that you are attempting to access does not exist '\
+ 'or you don\'t have permission to perform this action']
it 'does not delete cluster agent' do
expect { cluster_agent.reload }.not_to raise_error
diff --git a/spec/requests/api/graphql/mutations/container_registry/protection/rule/create_spec.rb b/spec/requests/api/graphql/mutations/container_registry/protection/rule/create_spec.rb
new file mode 100644
index 00000000000..0c708c3dc41
--- /dev/null
+++ b/spec/requests/api/graphql/mutations/container_registry/protection/rule/create_spec.rb
@@ -0,0 +1,180 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Creating the container registry protection rule', :aggregate_failures, feature_category: :container_registry do
+ include GraphqlHelpers
+
+ let_it_be(:project) { create(:project) }
+ let_it_be(:user) { create(:user, maintainer_projects: [project]) }
+
+ let(:container_registry_protection_rule_attributes) do
+ build_stubbed(:container_registry_protection_rule, project: project)
+ end
+
+ let(:kwargs) do
+ {
+ project_path: project.full_path,
+ container_path_pattern: container_registry_protection_rule_attributes.container_path_pattern,
+ push_protected_up_to_access_level: 'MAINTAINER',
+ delete_protected_up_to_access_level: 'MAINTAINER'
+ }
+ end
+
+ let(:mutation) do
+ graphql_mutation(:create_container_registry_protection_rule, kwargs,
+ <<~QUERY
+ containerRegistryProtectionRule {
+ id
+ containerPathPattern
+ }
+ clientMutationId
+ errors
+ QUERY
+ )
+ end
+
+ let(:mutation_response) { graphql_mutation_response(:create_container_registry_protection_rule) }
+
+ subject { post_graphql_mutation(mutation, current_user: user) }
+
+ shared_examples 'a successful response' do
+ it { subject.tap { expect_graphql_errors_to_be_empty } }
+
+ it do
+ subject
+
+ expect(mutation_response).to include(
+ 'errors' => be_blank,
+ 'containerRegistryProtectionRule' => {
+ 'id' => be_present,
+ 'containerPathPattern' => kwargs[:container_path_pattern]
+ }
+ )
+ end
+
+ it 'creates container registry protection rule in the database' do
+ expect { subject }.to change { ::ContainerRegistry::Protection::Rule.count }.by(1)
+
+ expect(::ContainerRegistry::Protection::Rule.where(project: project,
+ container_path_pattern: kwargs[:container_path_pattern])).to exist
+ end
+ end
+
+ shared_examples 'an erroneous response' do
+ it { expect { subject }.not_to change { ::ContainerRegistry::Protection::Rule.count } }
+ end
+
+ it_behaves_like 'a successful response'
+
+ context 'with invalid input fields `pushProtectedUpToAccessLevel` and `deleteProtectedUpToAccessLevel`' do
+ let(:kwargs) do
+ super().merge(
+ push_protected_up_to_access_level: 'UNKNOWN_ACCESS_LEVEL',
+ delete_protected_up_to_access_level: 'UNKNOWN_ACCESS_LEVEL'
+ )
+ end
+
+ it_behaves_like 'an erroneous response'
+
+ it {
+ subject
+
+ expect_graphql_errors_to_include([/pushProtectedUpToAccessLevel/, /deleteProtectedUpToAccessLevel/])
+ }
+ end
+
+ context 'with invalid input field `containerPathPattern`' do
+ let(:kwargs) do
+ super().merge(container_path_pattern: '')
+ end
+
+ it_behaves_like 'an erroneous response'
+
+ it { subject.tap { expect_graphql_errors_to_be_empty } }
+
+ it {
+ subject.tap do
+ expect(mutation_response['errors']).to eq ["Container path pattern can't be blank"]
+ end
+ }
+ end
+
+ context 'with existing containers protection rule' do
+ let_it_be(:existing_container_registry_protection_rule) do
+ create(:container_registry_protection_rule, project: project,
+ push_protected_up_to_access_level: Gitlab::Access::DEVELOPER)
+ end
+
+ context 'when container name pattern is slightly different' do
+ let(:kwargs) do
+ # The field `container_path_pattern` is unique; this is why we change the value in a minimum way
+ super().merge(
+ container_path_pattern: "#{existing_container_registry_protection_rule.container_path_pattern}-unique"
+ )
+ end
+
+ it_behaves_like 'a successful response'
+
+ it 'adds another container registry protection rule to the database' do
+ expect { subject }.to change { ::ContainerRegistry::Protection::Rule.count }.from(1).to(2)
+ end
+ end
+
+ context 'when field `container_path_pattern` is taken' do
+ let(:kwargs) do
+ super().merge(container_path_pattern: existing_container_registry_protection_rule.container_path_pattern,
+ push_protected_up_to_access_level: 'MAINTAINER')
+ end
+
+ it_behaves_like 'an erroneous response'
+
+ it { subject.tap { expect_graphql_errors_to_be_empty } }
+
+ it 'returns without error' do
+ subject
+
+ expect(mutation_response['errors']).to eq ['Container path pattern has already been taken']
+ end
+
+ it 'does not create new container protection rules' do
+ expect(::ContainerRegistry::Protection::Rule.where(project: project,
+ container_path_pattern: kwargs[:container_path_pattern],
+ push_protected_up_to_access_level: Gitlab::Access::MAINTAINER)).not_to exist
+ end
+ end
+ end
+
+ context 'when user does not have permission' do
+ let_it_be(:developer) { create(:user).tap { |u| project.add_developer(u) } }
+ let_it_be(:reporter) { create(:user).tap { |u| project.add_reporter(u) } }
+ let_it_be(:guest) { create(:user).tap { |u| project.add_guest(u) } }
+ let_it_be(:anonymous) { create(:user) }
+
+ where(:user) do
+ [ref(:developer), ref(:reporter), ref(:guest), ref(:anonymous)]
+ end
+
+ with_them do
+ it_behaves_like 'an erroneous response'
+
+ it { subject.tap { expect_graphql_errors_to_include(/you don't have permission to perform this action/) } }
+ end
+ end
+
+ context "when feature flag ':container_registry_protected_containers' disabled" do
+ before do
+ stub_feature_flags(container_registry_protected_containers: false)
+ end
+
+ it_behaves_like 'an erroneous response'
+
+ it { subject.tap { expect(::ContainerRegistry::Protection::Rule.where(project: project)).not_to exist } }
+
+ it 'returns error of disabled feature flag' do
+ subject.tap do
+ expect_graphql_errors_to_include(/'container_registry_protected_containers' feature flag is disabled/)
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/mutations/container_repository/destroy_tags_spec.rb b/spec/requests/api/graphql/mutations/container_repository/destroy_tags_spec.rb
index 0cb607e13ec..7ced22890df 100644
--- a/spec/requests/api/graphql/mutations/container_repository/destroy_tags_spec.rb
+++ b/spec/requests/api/graphql/mutations/container_repository/destroy_tags_spec.rb
@@ -27,7 +27,7 @@ RSpec.describe 'Destroying a container repository tags', feature_category: :cont
shared_examples 'destroying the container repository tags' do
before do
stub_delete_reference_requests(tags)
- expect_delete_tag_by_names(tags)
+ expect_delete_tags(tags)
allow_next_instance_of(ContainerRegistry::Client) do |client|
allow(client).to receive(:supports_tag_delete?).and_return(true)
end
diff --git a/spec/requests/api/graphql/mutations/design_management/delete_spec.rb b/spec/requests/api/graphql/mutations/design_management/delete_spec.rb
index 7ea32ae6d19..6f421abc489 100644
--- a/spec/requests/api/graphql/mutations/design_management/delete_spec.rb
+++ b/spec/requests/api/graphql/mutations/design_management/delete_spec.rb
@@ -47,28 +47,28 @@ RSpec.describe "deleting designs", feature_category: :design_management do
context 'the designs list is empty' do
it_behaves_like 'a failed request' do
let(:designs) { [] }
- let(:the_error) { a_string_matching %r/no filenames/ }
+ let(:the_error) { a_string_matching %r{no filenames} }
end
end
context 'the designs list contains filenames we cannot find' do
it_behaves_like 'a failed request' do
- let(:designs) { %w/foo bar baz/.map { |fn| double('file', filename: fn) } }
- let(:the_error) { a_string_matching %r/filenames were not found/ }
+ let(:designs) { %w[foo bar baz].map { |fn| double('file', filename: fn) } }
+ let(:the_error) { a_string_matching %r{filenames were not found} }
end
end
context 'the current user does not have developer access' do
it_behaves_like 'a failed request' do
let(:current_user) { create(:user) }
- let(:the_error) { a_string_matching %r/you don't have permission/ }
+ let(:the_error) { a_string_matching %r{you don't have permission} }
end
end
context "when the issue does not exist" do
it_behaves_like 'a failed request' do
let(:variables) { { iid: "1234567890" } }
- let(:the_error) { a_string_matching %r/does not exist/ }
+ let(:the_error) { a_string_matching %r{does not exist} }
end
end
diff --git a/spec/requests/api/graphql/mutations/design_management/upload_spec.rb b/spec/requests/api/graphql/mutations/design_management/upload_spec.rb
index 9b42b32c150..82a88a2c593 100644
--- a/spec/requests/api/graphql/mutations/design_management/upload_spec.rb
+++ b/spec/requests/api/graphql/mutations/design_management/upload_spec.rb
@@ -36,10 +36,11 @@ RSpec.describe "uploading designs", feature_category: :design_management do
end
it 'returns an error' do
- workhorse_post_with_file(api('/', current_user, version: 'graphql'),
- params: params,
- file_key: '1'
- )
+ workhorse_post_with_file(
+ api('/', current_user, version: 'graphql'),
+ params: params,
+ file_key: '1'
+ )
expect(response).to have_attributes(
code: eq('400'),
diff --git a/spec/requests/api/graphql/mutations/issues/link_alerts_spec.rb b/spec/requests/api/graphql/mutations/issues/link_alerts_spec.rb
index 85e21952f47..df6c20d6176 100644
--- a/spec/requests/api/graphql/mutations/issues/link_alerts_spec.rb
+++ b/spec/requests/api/graphql/mutations/issues/link_alerts_spec.rb
@@ -19,19 +19,21 @@ RSpec.describe 'Link alerts to an incident', feature_category: :incident_managem
alert_references: [alert1.to_reference, alert2.details_url]
}
- graphql_mutation(:issue_link_alerts, variables,
- <<-QL.strip_heredoc
- clientMutationId
- errors
- issue {
- iid
- alertManagementAlerts {
- nodes {
- iid
- }
- }
- }
- QL
+ graphql_mutation(
+ :issue_link_alerts,
+ variables,
+ <<-QL.strip_heredoc
+ clientMutationId
+ errors
+ issue {
+ iid
+ alertManagementAlerts {
+ nodes {
+ iid
+ }
+ }
+ }
+ QL
)
end
diff --git a/spec/requests/api/graphql/mutations/issues/move_spec.rb b/spec/requests/api/graphql/mutations/issues/move_spec.rb
index 7d9579067b6..24188d5341d 100644
--- a/spec/requests/api/graphql/mutations/issues/move_spec.rb
+++ b/spec/requests/api/graphql/mutations/issues/move_spec.rb
@@ -16,14 +16,16 @@ RSpec.describe 'Moving an issue', feature_category: :team_planning do
iid: issue.iid.to_s
}
- graphql_mutation(:issue_move, variables,
- <<-QL.strip_heredoc
- clientMutationId
- errors
- issue {
- title
- }
- QL
+ graphql_mutation(
+ :issue_move,
+ variables,
+ <<-QL.strip_heredoc
+ clientMutationId
+ errors
+ issue {
+ title
+ }
+ QL
)
end
diff --git a/spec/requests/api/graphql/mutations/issues/set_confidential_spec.rb b/spec/requests/api/graphql/mutations/issues/set_confidential_spec.rb
index c5e6901d8f8..c62995c0b9b 100644
--- a/spec/requests/api/graphql/mutations/issues/set_confidential_spec.rb
+++ b/spec/requests/api/graphql/mutations/issues/set_confidential_spec.rb
@@ -15,15 +15,17 @@ RSpec.describe 'Setting an issue as confidential', feature_category: :team_plann
project_path: project.full_path,
iid: issue.iid.to_s
}
- graphql_mutation(:issue_set_confidential, variables.merge(input),
- <<-QL.strip_heredoc
- clientMutationId
- errors
- issue {
- iid
- confidential
- }
- QL
+ graphql_mutation(
+ :issue_set_confidential,
+ variables.merge(input),
+ <<-QL.strip_heredoc
+ clientMutationId
+ errors
+ issue {
+ iid
+ confidential
+ }
+ QL
)
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 497ae1cc13f..cdab267162e 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
@@ -26,18 +26,20 @@ RSpec.describe 'Setting issues crm contacts', feature_category: :service_desk do
contact_ids: contact_ids
}
- graphql_mutation(:issue_set_crm_contacts, variables,
- <<-QL.strip_heredoc
- clientMutationId
- errors
- issue {
- customerRelationsContacts {
- nodes {
- id
- }
- }
- }
- QL
+ graphql_mutation(
+ :issue_set_crm_contacts,
+ variables,
+ <<-QL.strip_heredoc
+ clientMutationId
+ errors
+ issue {
+ customerRelationsContacts {
+ nodes {
+ id
+ }
+ }
+ }
+ QL
)
end
diff --git a/spec/requests/api/graphql/mutations/issues/set_due_date_spec.rb b/spec/requests/api/graphql/mutations/issues/set_due_date_spec.rb
index 1a5a64e4196..f7c5febe56f 100644
--- a/spec/requests/api/graphql/mutations/issues/set_due_date_spec.rb
+++ b/spec/requests/api/graphql/mutations/issues/set_due_date_spec.rb
@@ -15,15 +15,17 @@ RSpec.describe 'Setting Due Date of an issue', feature_category: :team_planning
project_path: project.full_path,
iid: issue.iid.to_s
}
- graphql_mutation(:issue_set_due_date, variables.merge(input),
- <<-QL.strip_heredoc
- clientMutationId
- errors
- issue {
- iid
- dueDate
- }
- QL
+ graphql_mutation(
+ :issue_set_due_date,
+ variables.merge(input),
+ <<-QL.strip_heredoc
+ clientMutationId
+ errors
+ issue {
+ iid
+ dueDate
+ }
+ QL
)
end
@@ -68,7 +70,7 @@ RSpec.describe 'Setting Due Date of an issue', feature_category: :team_planning
it 'returns an error' do
post_graphql_mutation(mutation, current_user: current_user)
- expect(graphql_errors).to include(a_hash_including('message' => /Arguments must be provided: dueDate/))
+ expect(graphql_errors).to include(a_hash_including('message' => 'issueSetDueDate has the wrong arguments'))
end
end
diff --git a/spec/requests/api/graphql/mutations/issues/set_locked_spec.rb b/spec/requests/api/graphql/mutations/issues/set_locked_spec.rb
index a8025894b1e..547ec280150 100644
--- a/spec/requests/api/graphql/mutations/issues/set_locked_spec.rb
+++ b/spec/requests/api/graphql/mutations/issues/set_locked_spec.rb
@@ -16,15 +16,17 @@ RSpec.describe 'Setting an issue as locked', feature_category: :team_planning do
project_path: project.full_path,
iid: issue.iid.to_s
}
- graphql_mutation(:issue_set_locked, variables.merge(input),
- <<-QL.strip_heredoc
- clientMutationId
- errors
- issue {
- iid
- discussionLocked
- }
- QL
+ graphql_mutation(
+ :issue_set_locked,
+ variables.merge(input),
+ <<-QL.strip_heredoc
+ clientMutationId
+ errors
+ issue {
+ iid
+ discussionLocked
+ }
+ QL
)
end
diff --git a/spec/requests/api/graphql/mutations/issues/set_severity_spec.rb b/spec/requests/api/graphql/mutations/issues/set_severity_spec.rb
index 77262c7f64f..d53b938a983 100644
--- a/spec/requests/api/graphql/mutations/issues/set_severity_spec.rb
+++ b/spec/requests/api/graphql/mutations/issues/set_severity_spec.rb
@@ -17,15 +17,17 @@ RSpec.describe 'Setting severity level of an incident', feature_category: :incid
iid: incident.iid.to_s
}
- graphql_mutation(:issue_set_severity, variables.merge(input),
- <<-QL.strip_heredoc
- clientMutationId
- errors
- issue {
- iid
- severity
- }
- QL
+ graphql_mutation(
+ :issue_set_severity,
+ variables.merge(input),
+ <<-QL.strip_heredoc
+ clientMutationId
+ errors
+ issue {
+ iid
+ severity
+ }
+ QL
)
end
diff --git a/spec/requests/api/graphql/mutations/issues/unlink_alerts_spec.rb b/spec/requests/api/graphql/mutations/issues/unlink_alerts_spec.rb
index 7f6f968b1dd..807afdfb812 100644
--- a/spec/requests/api/graphql/mutations/issues/unlink_alerts_spec.rb
+++ b/spec/requests/api/graphql/mutations/issues/unlink_alerts_spec.rb
@@ -21,19 +21,21 @@ RSpec.describe 'Unlink alert from an incident', feature_category: :incident_mana
alert_id: alert_to_unlink.to_global_id.to_s
}
- graphql_mutation(:issue_unlink_alert, variables,
- <<-QL.strip_heredoc
- clientMutationId
- errors
- issue {
- iid
- alertManagementAlerts {
- nodes {
- id
- }
- }
- }
- QL
+ graphql_mutation(
+ :issue_unlink_alert,
+ variables,
+ <<-QL.strip_heredoc
+ clientMutationId
+ errors
+ issue {
+ iid
+ alertManagementAlerts {
+ nodes {
+ id
+ }
+ }
+ }
+ QL
)
end
diff --git a/spec/requests/api/graphql/mutations/merge_requests/reviewer_rereview_spec.rb b/spec/requests/api/graphql/mutations/merge_requests/reviewer_rereview_spec.rb
index 7a1b3982111..ec82941b094 100644
--- a/spec/requests/api/graphql/mutations/merge_requests/reviewer_rereview_spec.rb
+++ b/spec/requests/api/graphql/mutations/merge_requests/reviewer_rereview_spec.rb
@@ -16,11 +16,13 @@ RSpec.describe 'Setting assignees of a merge request', feature_category: :code_r
project_path: project.full_path,
iid: merge_request.iid.to_s
}
- graphql_mutation(:merge_request_reviewer_rereview, variables.merge(input),
- <<-QL.strip_heredoc
- clientMutationId
- errors
- QL
+ graphql_mutation(
+ :merge_request_reviewer_rereview,
+ variables.merge(input),
+ <<-QL.strip_heredoc
+ clientMutationId
+ errors
+ QL
)
end
diff --git a/spec/requests/api/graphql/mutations/merge_requests/set_assignees_spec.rb b/spec/requests/api/graphql/mutations/merge_requests/set_assignees_spec.rb
index 4a7d1083f2e..cb7bac771b3 100644
--- a/spec/requests/api/graphql/mutations/merge_requests/set_assignees_spec.rb
+++ b/spec/requests/api/graphql/mutations/merge_requests/set_assignees_spec.rb
@@ -21,19 +21,21 @@ RSpec.describe 'Setting assignees of a merge request', :assume_throttled, featur
project_path: project.full_path,
iid: merge_request.iid.to_s
}
- graphql_mutation(:merge_request_set_assignees, variables.merge(input),
- <<-QL.strip_heredoc
- clientMutationId
- errors
- mergeRequest {
- id
- assignees {
- nodes {
- username
- }
- }
- }
- QL
+ graphql_mutation(
+ :merge_request_set_assignees,
+ variables.merge(input),
+ <<-QL.strip_heredoc
+ clientMutationId
+ errors
+ mergeRequest {
+ id
+ assignees {
+ nodes {
+ username
+ }
+ }
+ }
+ QL
)
end
diff --git a/spec/requests/api/graphql/mutations/merge_requests/set_draft_spec.rb b/spec/requests/api/graphql/mutations/merge_requests/set_draft_spec.rb
index 0c2e2975350..a2c5c235d25 100644
--- a/spec/requests/api/graphql/mutations/merge_requests/set_draft_spec.rb
+++ b/spec/requests/api/graphql/mutations/merge_requests/set_draft_spec.rb
@@ -15,15 +15,17 @@ RSpec.describe 'Setting Draft status of a merge request', feature_category: :cod
project_path: project.full_path,
iid: merge_request.iid.to_s
}
- graphql_mutation(:merge_request_set_draft, variables.merge(input),
- <<-QL.strip_heredoc
- clientMutationId
- errors
- mergeRequest {
- id
- title
- }
- QL
+ graphql_mutation(
+ :merge_request_set_draft,
+ variables.merge(input),
+ <<-QL.strip_heredoc
+ clientMutationId
+ errors
+ mergeRequest {
+ id
+ title
+ }
+ QL
)
end
diff --git a/spec/requests/api/graphql/mutations/merge_requests/set_labels_spec.rb b/spec/requests/api/graphql/mutations/merge_requests/set_labels_spec.rb
index e40a3cf7ce9..4ddd10b1734 100644
--- a/spec/requests/api/graphql/mutations/merge_requests/set_labels_spec.rb
+++ b/spec/requests/api/graphql/mutations/merge_requests/set_labels_spec.rb
@@ -17,19 +17,21 @@ RSpec.describe 'Setting labels of a merge request' do
project_path: project.full_path,
iid: merge_request.iid.to_s
}
- graphql_mutation(:merge_request_set_labels, variables.merge(input),
- <<-QL.strip_heredoc
- clientMutationId
- errors
- mergeRequest {
- id
- labels {
- nodes {
- id
- }
- }
- }
- QL
+ graphql_mutation(
+ :merge_request_set_labels,
+ variables.merge(input),
+ <<-QL.strip_heredoc
+ clientMutationId
+ errors
+ mergeRequest {
+ id
+ labels {
+ nodes {
+ id
+ }
+ }
+ }
+ QL
)
end
diff --git a/spec/requests/api/graphql/mutations/merge_requests/set_locked_spec.rb b/spec/requests/api/graphql/mutations/merge_requests/set_locked_spec.rb
index 73a38adf723..a6ddb9beb5c 100644
--- a/spec/requests/api/graphql/mutations/merge_requests/set_locked_spec.rb
+++ b/spec/requests/api/graphql/mutations/merge_requests/set_locked_spec.rb
@@ -15,15 +15,17 @@ RSpec.describe 'Setting locked status of a merge request', feature_category: :co
project_path: project.full_path,
iid: merge_request.iid.to_s
}
- graphql_mutation(:merge_request_set_locked, variables.merge(input),
- <<-QL.strip_heredoc
- clientMutationId
- errors
- mergeRequest {
- id
- discussionLocked
- }
- QL
+ graphql_mutation(
+ :merge_request_set_locked,
+ variables.merge(input),
+ <<-QL.strip_heredoc
+ clientMutationId
+ errors
+ mergeRequest {
+ id
+ discussionLocked
+ }
+ QL
)
end
diff --git a/spec/requests/api/graphql/mutations/merge_requests/set_milestone_spec.rb b/spec/requests/api/graphql/mutations/merge_requests/set_milestone_spec.rb
index 1898ee5a62d..9debfbd474b 100644
--- a/spec/requests/api/graphql/mutations/merge_requests/set_milestone_spec.rb
+++ b/spec/requests/api/graphql/mutations/merge_requests/set_milestone_spec.rb
@@ -16,17 +16,19 @@ RSpec.describe 'Setting milestone of a merge request', feature_category: :code_r
project_path: project.full_path,
iid: merge_request.iid.to_s
}
- graphql_mutation(:merge_request_set_milestone, variables.merge(input),
- <<-QL.strip_heredoc
- clientMutationId
- errors
- mergeRequest {
- id
- milestone {
- id
- }
- }
- QL
+ graphql_mutation(
+ :merge_request_set_milestone,
+ variables.merge(input),
+ <<-QL.strip_heredoc
+ clientMutationId
+ errors
+ mergeRequest {
+ id
+ milestone {
+ id
+ }
+ }
+ QL
)
end
diff --git a/spec/requests/api/graphql/mutations/merge_requests/set_reviewers_spec.rb b/spec/requests/api/graphql/mutations/merge_requests/set_reviewers_spec.rb
index fd87112be33..c9efba689c2 100644
--- a/spec/requests/api/graphql/mutations/merge_requests/set_reviewers_spec.rb
+++ b/spec/requests/api/graphql/mutations/merge_requests/set_reviewers_spec.rb
@@ -21,19 +21,21 @@ RSpec.describe 'Setting reviewers of a merge request', :assume_throttled, featur
project_path: project.full_path,
iid: merge_request.iid.to_s
}
- graphql_mutation(:merge_request_set_reviewers, variables.merge(input),
- <<-QL.strip_heredoc
- clientMutationId
- errors
- mergeRequest {
- id
- reviewers {
- nodes {
- username
- }
- }
- }
- QL
+ graphql_mutation(
+ :merge_request_set_reviewers,
+ variables.merge(input),
+ <<-QL.strip_heredoc
+ clientMutationId
+ errors
+ mergeRequest {
+ id
+ reviewers {
+ nodes {
+ username
+ }
+ }
+ }
+ QL
)
end
diff --git a/spec/requests/api/graphql/mutations/merge_requests/set_time_estimate_spec.rb b/spec/requests/api/graphql/mutations/merge_requests/set_time_estimate_spec.rb
index 6bc130a97cf..541cdf0660d 100644
--- a/spec/requests/api/graphql/mutations/merge_requests/set_time_estimate_spec.rb
+++ b/spec/requests/api/graphql/mutations/merge_requests/set_time_estimate_spec.rb
@@ -17,8 +17,11 @@ RSpec.describe 'Setting time estimate of a merge request', feature_category: :co
let(:extra_params) { { project_path: project.full_path } }
let(:input_params) { input.merge(extra_params) }
- let(:mutation) { graphql_mutation(:merge_request_update, input_params, nil, ['productAnalyticsState']) }
let(:mutation_response) { graphql_mutation_response(:merge_request_update) }
+ let(:mutation) do
+ # exclude codequalityReportsComparer because it's behind a feature flag
+ graphql_mutation(:merge_request_update, input_params, nil, %w[productAnalyticsState codequalityReportsComparer])
+ end
context 'when the user is not allowed to update a merge request' do
before_all do
diff --git a/spec/requests/api/graphql/mutations/merge_requests/update_spec.rb b/spec/requests/api/graphql/mutations/merge_requests/update_spec.rb
index 48db23569b6..ef21f77d818 100644
--- a/spec/requests/api/graphql/mutations/merge_requests/update_spec.rb
+++ b/spec/requests/api/graphql/mutations/merge_requests/update_spec.rb
@@ -12,8 +12,11 @@ RSpec.describe 'Update of an existing merge request', feature_category: :code_re
let(:input) { { 'iid' => merge_request.iid.to_s } }
let(:extra_params) { { project_path: project.full_path } }
let(:input_params) { input.merge(extra_params) }
- let(:mutation) { graphql_mutation(:merge_request_update, input_params, nil, ['productAnalyticsState']) }
let(:mutation_response) { graphql_mutation_response(:merge_request_update) }
+ let(:mutation) do
+ # exclude codequalityReportsComparer because it's behind a feature flag
+ graphql_mutation(:merge_request_update, input_params, nil, %w[productAnalyticsState codequalityReportsComparer])
+ end
context 'when the user is not allowed to update the merge request' do
it_behaves_like 'a mutation that returns a top-level access error'
@@ -28,5 +31,17 @@ RSpec.describe 'Update of an existing merge request', feature_category: :code_re
let(:resource) { merge_request }
let(:mutation_name) { 'mergeRequestUpdate' }
end
+
+ context 'when required arguments are missing' do
+ let(:input_params) { {} }
+
+ it_behaves_like 'a mutation that returns top-level errors' do
+ let(:match_errors) do
+ include(end_with(
+ 'invalid value for projectPath (Expected value to not be null), iid (Expected value to not be null)'
+ ))
+ end
+ end
+ end
end
end
diff --git a/spec/requests/api/graphql/mutations/namespace/package_settings/update_spec.rb b/spec/requests/api/graphql/mutations/namespace/package_settings/update_spec.rb
index 480e184a60c..738dc3078e7 100644
--- a/spec/requests/api/graphql/mutations/namespace/package_settings/update_spec.rb
+++ b/spec/requests/api/graphql/mutations/namespace/package_settings/update_spec.rb
@@ -129,26 +129,6 @@ RSpec.describe 'Updating the package settings', feature_category: :package_regis
it_behaves_like 'returning a success'
it_behaves_like 'rejecting invalid regex'
-
- context 'when nuget_duplicates_option FF is disabled' do
- let(:params) do
- {
- namespace_path: namespace.full_path,
- 'nugetDuplicatesAllowed' => false
- }
- end
-
- before do
- stub_feature_flags(nuget_duplicates_option: false)
- end
-
- it 'raises an error', :aggregate_failures do
- subject
-
- expect(graphql_errors.size).to eq(1)
- expect(graphql_errors.first['message']).to include('feature flag is disabled')
- end
- end
end
RSpec.shared_examples 'accepting the mutation request creating the package settings' do
diff --git a/spec/requests/api/graphql/mutations/notes/create/note_spec.rb b/spec/requests/api/graphql/mutations/notes/create/note_spec.rb
index 37bcdf61d23..33d840cafd7 100644
--- a/spec/requests/api/graphql/mutations/notes/create/note_spec.rb
+++ b/spec/requests/api/graphql/mutations/notes/create/note_spec.rb
@@ -5,13 +5,15 @@ require 'spec_helper'
RSpec.describe 'Adding a Note', feature_category: :team_planning do
include GraphqlHelpers
- let_it_be(:current_user) { create(:user) }
-
+ let_it_be(:user) { create(:user) }
+ let_it_be(:group) { create(:group).tap { |g| g.add_developer(developer) } }
+ let_it_be_with_reload(:project) { create(:project, :repository, group: group) }
+ let_it_be(:developer) { create(:user).tap { |u| group.add_developer(u) } }
let(:noteable) { create(:merge_request, source_project: project, target_project: project) }
- let(:project) { create(:project, :repository) }
let(:discussion) { nil }
let(:head_sha) { nil }
let(:body) { 'Body text' }
+ let(:current_user) { user }
let(:mutation) do
variables = {
noteable_id: GitlabSchema.id_from_object(noteable).to_s,
@@ -30,9 +32,7 @@ RSpec.describe 'Adding a Note', feature_category: :team_planning do
it_behaves_like 'a Note mutation when the user does not have permission'
context 'when the user has permission' do
- before do
- project.add_developer(current_user)
- end
+ let(:current_user) { developer }
it_behaves_like 'a working GraphQL mutation'
@@ -78,8 +78,10 @@ RSpec.describe 'Adding a Note', feature_category: :team_planning do
end
context 'for an issue' do
- let(:noteable) { create(:issue, project: project) }
+ let_it_be_with_reload(:issue) { create(:issue, project: project) }
+ let(:noteable) { issue }
let(:mutation) { graphql_mutation(:create_note, variables) }
+ let(:variables_extra) { {} }
let(:variables) do
{
noteable_id: GitlabSchema.id_from_object(noteable).to_s,
@@ -87,10 +89,6 @@ RSpec.describe 'Adding a Note', feature_category: :team_planning do
}.merge(variables_extra)
end
- before do
- project.add_developer(current_user)
- end
-
context 'when using internal param' do
let(:variables_extra) { { internal: true } }
@@ -104,8 +102,8 @@ RSpec.describe 'Adding a Note', feature_category: :team_planning do
end
context 'as work item' do
- let_it_be(:project) { create(:project) }
- let_it_be(:noteable) { create(:work_item, project: project) }
+ let_it_be_with_reload(:work_item) { create(:work_item, :task, project: project) }
+ let(:noteable) { work_item }
context 'when using internal param' do
let(:variables_extra) { { internal: true } }
@@ -120,10 +118,8 @@ RSpec.describe 'Adding a Note', feature_category: :team_planning do
end
context 'without notes widget' do
- let(:variables_extra) { {} }
-
before do
- WorkItems::Type.default_by_type(:issue).widget_definitions.find_by_widget_type(:notes)
+ WorkItems::Type.default_by_type(:task).widget_definitions.find_by_widget_type(:notes)
.update!(disabled: true)
end
@@ -133,10 +129,6 @@ RSpec.describe 'Adding a Note', feature_category: :team_planning do
end
context 'when body contains quick actions' do
- let_it_be(:noteable) { create(:work_item, :task, project: project) }
-
- let(:variables_extra) { {} }
-
it_behaves_like 'work item supports labels widget updates via quick actions'
it_behaves_like 'work item does not support labels widget updates via quick actions'
it_behaves_like 'work item supports assignee widget updates via quick actions'
@@ -145,6 +137,13 @@ RSpec.describe 'Adding a Note', feature_category: :team_planning do
it_behaves_like 'work item does not support start and due date widget updates via quick actions'
it_behaves_like 'work item supports type change via quick actions'
end
+
+ context 'when work item is directly associated with a group' do
+ let_it_be_with_reload(:group_work_item) { create(:work_item, :group_level, :task, namespace: group) }
+ let(:noteable) { group_work_item }
+
+ it_behaves_like 'a Note mutation that creates a Note'
+ end
end
end
@@ -152,10 +151,6 @@ RSpec.describe 'Adding a Note', feature_category: :team_planning do
let(:head_sha) { noteable.diff_head_sha }
let(:body) { '/merge' }
- before do
- project.add_developer(current_user)
- end
-
# NOTE: Known issue https://gitlab.com/gitlab-org/gitlab/-/issues/346557
it 'returns a nil note and info about the command in errors' do
post_graphql_mutation(mutation, current_user: current_user)
diff --git a/spec/requests/api/graphql/mutations/notes/update/image_diff_note_spec.rb b/spec/requests/api/graphql/mutations/notes/update/image_diff_note_spec.rb
index a5cd3c8b019..3f071a6d987 100644
--- a/spec/requests/api/graphql/mutations/notes/update/image_diff_note_spec.rb
+++ b/spec/requests/api/graphql/mutations/notes/update/image_diff_note_spec.rb
@@ -37,11 +37,13 @@ RSpec.describe 'Updating an image DiffNote', feature_category: :team_planning do
end
let!(:diff_note) do
- create(:image_diff_note_on_merge_request,
- noteable: noteable,
- project: noteable.project,
- note: original_body,
- position: original_position)
+ create(
+ :image_diff_note_on_merge_request,
+ noteable: noteable,
+ project: noteable.project,
+ note: original_body,
+ position: original_position
+ )
end
let(:mutation) do
diff --git a/spec/requests/api/graphql/mutations/organizations/create_spec.rb b/spec/requests/api/graphql/mutations/organizations/create_spec.rb
new file mode 100644
index 00000000000..ac6b04104ba
--- /dev/null
+++ b/spec/requests/api/graphql/mutations/organizations/create_spec.rb
@@ -0,0 +1,64 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Mutations::Organizations::Create, feature_category: :cell do
+ include GraphqlHelpers
+
+ let_it_be(:user) { create(:user) }
+
+ let(:mutation) { graphql_mutation(:organization_create, params) }
+ let(:name) { 'Name' }
+ let(:path) { 'path' }
+ let(:params) do
+ {
+ name: name,
+ path: path
+ }
+ end
+
+ subject(:create_organization) { post_graphql_mutation(mutation, current_user: current_user) }
+
+ it { expect(described_class).to require_graphql_authorizations(:create_organization) }
+
+ def mutation_response
+ graphql_mutation_response(:organization_create)
+ end
+
+ context 'when the user does not have permission' do
+ let(:current_user) { nil }
+
+ it_behaves_like 'a mutation that returns a top-level access error'
+
+ it 'does not create an organization' do
+ expect { create_organization }.not_to change { Organizations::Organization.count }
+ end
+ end
+
+ context 'when the user has permission' do
+ let(:current_user) { user }
+
+ context 'when the params are invalid' do
+ let(:name) { '' }
+
+ it 'returns the validation error' do
+ create_organization
+
+ expect(mutation_response).to include('errors' => ["Name can't be blank"])
+ end
+ end
+
+ it 'creates an organization' do
+ expect { create_organization }.to change { Organizations::Organization.count }.by(1)
+ end
+
+ it 'returns the new organization' do
+ create_organization
+
+ expect(graphql_data_at(:organization_create, :organization)).to match a_hash_including(
+ 'name' => name,
+ 'path' => path
+ )
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/mutations/packages/cleanup/policy/update_spec.rb b/spec/requests/api/graphql/mutations/packages/cleanup/policy/update_spec.rb
index 2540e06be9a..5843109f356 100644
--- a/spec/requests/api/graphql/mutations/packages/cleanup/policy/update_spec.rb
+++ b/spec/requests/api/graphql/mutations/packages/cleanup/policy/update_spec.rb
@@ -17,14 +17,16 @@ RSpec.describe 'Updating the packages cleanup policy', feature_category: :packag
end
let(:mutation) do
- graphql_mutation(:update_packages_cleanup_policy, params,
- <<~QUERY
- packagesCleanupPolicy {
- keepNDuplicatedPackageFiles
- nextRunAt
- }
- errors
- QUERY
+ graphql_mutation(
+ :update_packages_cleanup_policy,
+ params,
+ <<~QUERY
+ packagesCleanupPolicy {
+ keepNDuplicatedPackageFiles
+ nextRunAt
+ }
+ errors
+ QUERY
)
end
diff --git a/spec/requests/api/graphql/mutations/packages/protection/rule/create_spec.rb b/spec/requests/api/graphql/mutations/packages/protection/rule/create_spec.rb
index b0c8526fa1c..ae5b6a5af95 100644
--- a/spec/requests/api/graphql/mutations/packages/protection/rule/create_spec.rb
+++ b/spec/requests/api/graphql/mutations/packages/protection/rule/create_spec.rb
@@ -4,7 +4,6 @@ require 'spec_helper'
RSpec.describe 'Creating the packages protection rule', :aggregate_failures, feature_category: :package_registry do
include GraphqlHelpers
- using RSpec::Parameterized::TableSyntax
let_it_be(:project) { create(:project) }
let_it_be(:user) { create(:user, maintainer_projects: [project]) }
@@ -15,151 +14,162 @@ RSpec.describe 'Creating the packages protection rule', :aggregate_failures, fea
{
project_path: project.full_path,
package_name_pattern: package_protection_rule_attributes.package_name_pattern,
- package_type: "NPM",
- push_protected_up_to_access_level: "MAINTAINER"
+ package_type: 'NPM',
+ push_protected_up_to_access_level: 'MAINTAINER'
}
end
let(:mutation) do
graphql_mutation(:create_packages_protection_rule, kwargs,
<<~QUERY
- clientMutationId
+ packageProtectionRule {
+ id
+ packageNamePattern
+ packageType
+ pushProtectedUpToAccessLevel
+ }
errors
QUERY
)
end
- let(:mutation_response) { graphql_mutation_response(:create_packages_protection_rule) }
+ let(:mutation_response_package_protection_rule) do
+ graphql_data_at(:createPackagesProtectionRule, :packageProtectionRule)
+ end
- describe 'post graphql mutation' do
- subject { post_graphql_mutation(mutation, current_user: user) }
+ let(:mutation_response_errors) { graphql_data_at(:createPackagesProtectionRule, :errors) }
- context 'without existing packages protection rule' do
- it 'returns without error' do
- subject
+ subject { post_graphql_mutation(mutation, current_user: user) }
- expect_graphql_errors_to_be_empty
- end
+ shared_examples 'a successful response' do
+ it 'returns without error' do
+ subject
- it 'returns the created packages protection rule' do
- expect { subject }.to change { ::Packages::Protection::Rule.count }.by(1)
+ expect_graphql_errors_to_be_empty
+ expect(mutation_response_errors).to be_empty
+ end
- expect_graphql_errors_to_be_empty
- expect(Packages::Protection::Rule.where(project: project).count).to eq 1
+ it 'returns the created packages protection rule' do
+ subject
- expect(Packages::Protection::Rule.where(project: project,
- package_name_pattern: kwargs[:package_name_pattern])).to exist
- end
+ expect(mutation_response_package_protection_rule).to include(
+ 'id' => be_present,
+ 'packageNamePattern' => kwargs[:package_name_pattern],
+ 'packageType' => kwargs[:package_type],
+ 'pushProtectedUpToAccessLevel' => kwargs[:push_protected_up_to_access_level]
+ )
+ end
- context 'when invalid fields are given' do
- let(:kwargs) do
- {
- project_path: project.full_path,
- package_name_pattern: '',
- package_type: 'UNKNOWN_PACKAGE_TYPE',
- push_protected_up_to_access_level: 'UNKNOWN_ACCESS_LEVEL'
- }
- end
-
- it 'returns error about required argument' do
- subject
-
- expect_graphql_errors_to_include(/was provided invalid value for packageType/)
- expect_graphql_errors_to_include(/pushProtectedUpToAccessLevel/)
- end
- end
+ it 'creates one package protection rule' do
+ expect { subject }.to change { ::Packages::Protection::Rule.count }.by(1)
+
+ expect(Packages::Protection::Rule.last).to have_attributes(
+ project: project,
+ package_name_pattern: kwargs[:package_name_pattern],
+ package_type: kwargs[:package_type].downcase,
+ push_protected_up_to_access_level: kwargs[:push_protected_up_to_access_level].downcase
+ )
end
+ end
- context 'when user does not have permission' do
- let_it_be(:developer) { create(:user).tap { |u| project.add_developer(u) } }
- let_it_be(:reporter) { create(:user).tap { |u| project.add_reporter(u) } }
- let_it_be(:guest) { create(:user).tap { |u| project.add_guest(u) } }
- let_it_be(:anonymous) { create(:user) }
+ shared_examples 'an erroneous response' do
+ it 'does not create one package protection rule' do
+ expect { subject }.not_to change { ::Packages::Protection::Rule.count }
+ end
+ end
- where(:user) do
- [ref(:developer), ref(:reporter), ref(:guest), ref(:anonymous)]
- end
+ it_behaves_like 'a successful response'
- with_them do
- it 'returns an error' do
- expect { subject }.not_to change { ::Packages::Protection::Rule.count }
+ context 'with invalid kwargs leading to error from graphql' do
+ let(:kwargs) do
+ super().merge!(
+ package_name_pattern: '',
+ package_type: 'UNKNOWN_PACKAGE_TYPE',
+ push_protected_up_to_access_level: 'UNKNOWN_ACCESS_LEVEL'
+ )
+ end
- expect_graphql_errors_to_include(/you don't have permission to perform this action/)
- end
- end
+ it_behaves_like 'an erroneous response'
+
+ it 'returns error about required argument' do
+ subject
+
+ expect_graphql_errors_to_include(/was provided invalid value for packageType/)
+ expect_graphql_errors_to_include(/pushProtectedUpToAccessLevel/)
end
+ end
- context 'with existing packages protection rule' do
- let_it_be(:existing_package_protection_rule) do
- create(:package_protection_rule, project: project, push_protected_up_to_access_level: Gitlab::Access::DEVELOPER)
- end
+ context 'with invalid kwargs leading to error from business model' do
+ let(:kwargs) { super().merge!(package_name_pattern: '') }
- context 'when package name pattern is slightly different' do
- let(:kwargs) do
- {
- project_path: project.full_path,
- # The field `package_name_pattern` is unique; this is why we change the value in a minimum way
- package_name_pattern: "#{existing_package_protection_rule.package_name_pattern}-unique",
- package_type: "NPM",
- push_protected_up_to_access_level: "DEVELOPER"
- }
- end
-
- it 'returns the created packages protection rule' do
- expect { subject }.to change { ::Packages::Protection::Rule.count }.by(1)
-
- expect(Packages::Protection::Rule.where(project: project).count).to eq 2
- expect(Packages::Protection::Rule.where(project: project,
- package_name_pattern: kwargs[:package_name_pattern])).to exist
- end
-
- it 'returns without error' do
- subject
-
- expect_graphql_errors_to_be_empty
- end
- end
+ it_behaves_like 'an erroneous response'
- context 'when field `package_name_pattern` is taken' do
- let(:kwargs) do
- {
- project_path: project.full_path,
- package_name_pattern: existing_package_protection_rule.package_name_pattern,
- package_type: 'NPM',
- push_protected_up_to_access_level: 'MAINTAINER'
- }
- end
-
- it 'returns without error' do
- subject
-
- expect(mutation_response).to include 'errors' => ['Package name pattern has already been taken']
- end
-
- it 'does not create new package protection rules' do
- expect { subject }.to change { Packages::Protection::Rule.count }.by(0)
-
- expect(Packages::Protection::Rule.where(project: project,
- package_name_pattern: kwargs[:package_name_pattern],
- push_protected_up_to_access_level: Gitlab::Access::MAINTAINER)).not_to exist
- end
- end
+ it 'returns an error' do
+ subject.tap { expect(mutation_response_errors).to include(/Package name pattern can't be blank/) }
end
+ end
- context "when feature flag ':packages_protected_packages' disabled" do
- before do
- stub_feature_flags(packages_protected_packages: false)
+ context 'with existing packages protection rule' do
+ let_it_be(:existing_package_protection_rule) do
+ create(:package_protection_rule, project: project, push_protected_up_to_access_level: :maintainer)
+ end
+
+ let(:kwargs) { super().merge!(package_name_pattern: existing_package_protection_rule.package_name_pattern) }
+
+ it_behaves_like 'an erroneous response'
+
+ it 'returns an error' do
+ subject.tap { expect(mutation_response_errors).to include(/Package name pattern has already been taken/) }
+ end
+
+ context 'when field `package_name_pattern` is different than existing one' do
+ let(:kwargs) do
+ # The field `package_name_pattern` is unique; this is why we change the value in a minimum way
+ super().merge!(package_name_pattern: "#{existing_package_protection_rule.package_name_pattern}-unique")
end
- it 'does not create any package protection rules' do
- expect { subject }.to change { Packages::Protection::Rule.count }.by(0)
+ it_behaves_like 'a successful response'
+ end
+
+ context 'when field `push_protected_up_to_access_level` is different than existing one' do
+ let(:kwargs) { super().merge!(push_protected_up_to_access_level: 'DEVELOPER') }
+
+ it_behaves_like 'an erroneous response'
- expect(Packages::Protection::Rule.where(project: project)).not_to exist
+ it 'returns an error' do
+ subject.tap { expect(mutation_response_errors).to include(/Package name pattern has already been taken/) }
end
+ end
+ end
- it 'returns error of disabled feature flag' do
- subject.tap { expect_graphql_errors_to_include(/'packages_protected_packages' feature flag is disabled/) }
+ context 'when user does not have permission' do
+ let_it_be(:anonymous) { create(:user) }
+ let_it_be(:developer) { create(:user).tap { |u| project.add_developer(u) } }
+ let_it_be(:guest) { create(:user).tap { |u| project.add_guest(u) } }
+ let_it_be(:reporter) { create(:user).tap { |u| project.add_reporter(u) } }
+
+ where(:user) do
+ [ref(:developer), ref(:reporter), ref(:guest), ref(:anonymous)]
+ end
+
+ with_them do
+ it_behaves_like 'an erroneous response'
+
+ it 'returns an error' do
+ subject.tap { expect_graphql_errors_to_include(/you don't have permission to perform this action/) }
end
end
end
+
+ context "when feature flag ':packages_protected_packages' disabled" do
+ before do
+ stub_feature_flags(packages_protected_packages: false)
+ end
+
+ it_behaves_like 'an erroneous response'
+
+ it 'returns error of disabled feature flag' do
+ subject.tap { expect_graphql_errors_to_include(/'packages_protected_packages' feature flag is disabled/) }
+ end
+ end
end
diff --git a/spec/requests/api/graphql/mutations/packages/protection/rule/delete_spec.rb b/spec/requests/api/graphql/mutations/packages/protection/rule/delete_spec.rb
new file mode 100644
index 00000000000..1d94d520674
--- /dev/null
+++ b/spec/requests/api/graphql/mutations/packages/protection/rule/delete_spec.rb
@@ -0,0 +1,88 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Deleting a package protection rule', :aggregate_failures, feature_category: :package_registry do
+ include GraphqlHelpers
+
+ let_it_be(:project) { create(:project, :repository) }
+ let_it_be_with_refind(:package_protection_rule) { create(:package_protection_rule, project: project) }
+ let_it_be(:current_user) { create(:user, maintainer_projects: [project]) }
+
+ let(:mutation) { graphql_mutation(:delete_packages_protection_rule, input) }
+ let(:mutation_response) { graphql_mutation_response(:delete_packages_protection_rule) }
+ let(:input) { { id: package_protection_rule.to_global_id } }
+
+ subject { post_graphql_mutation(mutation, current_user: current_user) }
+
+ shared_examples 'an erroneous reponse' do
+ it { subject.tap { expect(mutation_response).to be_blank } }
+ it { expect { subject }.not_to change { ::Packages::Protection::Rule.count } }
+ end
+
+ it_behaves_like 'a working GraphQL mutation'
+
+ it 'responds with deleted package protection rule' do
+ subject
+
+ expect(mutation_response).to include(
+ 'errors' => be_blank,
+ 'packageProtectionRule' => {
+ 'id' => package_protection_rule.to_global_id.to_s,
+ 'packageNamePattern' => package_protection_rule.package_name_pattern,
+ 'packageType' => package_protection_rule.package_type.upcase,
+ 'pushProtectedUpToAccessLevel' => package_protection_rule.push_protected_up_to_access_level.upcase
+ }
+ )
+ end
+
+ it { is_expected.tap { expect_graphql_errors_to_be_empty } }
+ it { expect { subject }.to change { ::Packages::Protection::Rule.count }.from(1).to(0) }
+
+ context 'with existing package protection rule belonging to other project' do
+ let_it_be(:package_protection_rule) do
+ create(:package_protection_rule, package_name_pattern: 'protection_rule_other_project')
+ end
+
+ it_behaves_like 'an erroneous reponse'
+
+ it { subject.tap { expect_graphql_errors_to_include(/you don't have permission to perform this action/) } }
+ end
+
+ context 'with deleted package protection rule' do
+ let!(:package_protection_rule) do
+ create(:package_protection_rule, project: project, package_name_pattern: 'protection_rule_deleted').destroy!
+ end
+
+ it_behaves_like 'an erroneous reponse'
+
+ it { subject.tap { expect_graphql_errors_to_include(/you don't have permission to perform this action/) } }
+ end
+
+ context 'when current_user does not have permission' do
+ let_it_be(:developer) { create(:user).tap { |u| project.add_developer(u) } }
+ let_it_be(:reporter) { create(:user).tap { |u| project.add_reporter(u) } }
+ let_it_be(:guest) { create(:user).tap { |u| project.add_guest(u) } }
+ let_it_be(:anonymous) { create(:user) }
+
+ where(:current_user) do
+ [ref(:developer), ref(:reporter), ref(:guest), ref(:anonymous)]
+ end
+
+ with_them do
+ it_behaves_like 'an erroneous reponse'
+
+ it { subject.tap { expect_graphql_errors_to_include(/you don't have permission to perform this action/) } }
+ end
+ end
+
+ context "when feature flag ':packages_protected_packages' disabled" do
+ before do
+ stub_feature_flags(packages_protected_packages: false)
+ end
+
+ it_behaves_like 'an erroneous reponse'
+
+ it { subject.tap { expect_graphql_errors_to_include(/'packages_protected_packages' feature flag is disabled/) } }
+ end
+end
diff --git a/spec/requests/api/graphql/mutations/release_asset_links/update_spec.rb b/spec/requests/api/graphql/mutations/release_asset_links/update_spec.rb
index 45028cba3ae..fdd4de865ad 100644
--- a/spec/requests/api/graphql/mutations/release_asset_links/update_spec.rb
+++ b/spec/requests/api/graphql/mutations/release_asset_links/update_spec.rb
@@ -10,12 +10,14 @@ RSpec.describe 'Updating an existing release asset link', feature_category: :rel
let_it_be(:developer) { create(:user).tap { |u| project.add_developer(u) } }
let_it_be(:release_link) do
- create(:release_link,
- release: release,
- name: 'link name',
- url: 'https://example.com/url',
- filepath: '/permanent/path',
- link_type: 'package')
+ create(
+ :release_link,
+ release: release,
+ name: 'link name',
+ url: 'https://example.com/url',
+ filepath: '/permanent/path',
+ link_type: 'package'
+ )
end
let(:current_user) { developer }
diff --git a/spec/requests/api/graphql/mutations/snippets/create_spec.rb b/spec/requests/api/graphql/mutations/snippets/create_spec.rb
index a6d727ae6d3..7094cb807b2 100644
--- a/spec/requests/api/graphql/mutations/snippets/create_spec.rb
+++ b/spec/requests/api/graphql/mutations/snippets/create_spec.rb
@@ -42,7 +42,7 @@ RSpec.describe 'Creating a Snippet', feature_category: :source_code_management d
let(:current_user) { nil }
it_behaves_like 'a mutation that returns top-level errors',
- errors: [Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR]
+ errors: [Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR]
it 'does not create the Snippet' do
expect do
@@ -122,7 +122,7 @@ RSpec.describe 'Creating a Snippet', feature_category: :source_code_management d
let(:project_path) { 'foobar' }
it_behaves_like 'a mutation that returns top-level errors',
- errors: [Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR]
+ errors: [Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR]
end
context 'when the feature is disabled' do
@@ -131,7 +131,7 @@ RSpec.describe 'Creating a Snippet', feature_category: :source_code_management d
end
it_behaves_like 'a mutation that returns top-level errors',
- errors: [Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR]
+ errors: [Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR]
end
it_behaves_like 'snippet edit usage data counters'
@@ -169,8 +169,8 @@ RSpec.describe 'Creating a Snippet', feature_category: :source_code_management d
end
it_behaves_like 'expected files argument', nil, nil
- it_behaves_like 'expected files argument', %w(foo bar), %w(foo bar)
- it_behaves_like 'expected files argument', 'foo', %w(foo)
+ it_behaves_like 'expected files argument', %w[foo bar], %w[foo bar]
+ it_behaves_like 'expected files argument', 'foo', %w[foo]
context 'when files has an invalid value' do
let(:uploaded_files) { [1] }
diff --git a/spec/requests/api/graphql/mutations/snippets/destroy_spec.rb b/spec/requests/api/graphql/mutations/snippets/destroy_spec.rb
index c3f818b6627..7b0de7a9fba 100644
--- a/spec/requests/api/graphql/mutations/snippets/destroy_spec.rb
+++ b/spec/requests/api/graphql/mutations/snippets/destroy_spec.rb
@@ -24,7 +24,7 @@ RSpec.describe 'Destroying a Snippet', feature_category: :source_code_management
let(:current_user) { create(:user) }
it_behaves_like 'a mutation that returns top-level errors',
- errors: [Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR]
+ errors: [Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR]
it 'does not destroy the Snippet' do
expect do
@@ -53,7 +53,7 @@ RSpec.describe 'Destroying a Snippet', feature_category: :source_code_management
let!(:snippet_gid) { project.to_gid.to_s }
it 'returns an error' do
- err_message = %["#{snippet_gid}" does not represent an instance of Snippet]
+ err_message = %("#{snippet_gid}" does not represent an instance of Snippet)
post_graphql_mutation(mutation, current_user: current_user)
diff --git a/spec/requests/api/graphql/mutations/snippets/mark_as_spam_spec.rb b/spec/requests/api/graphql/mutations/snippets/mark_as_spam_spec.rb
index 9a8c027da8a..6fd41437ce4 100644
--- a/spec/requests/api/graphql/mutations/snippets/mark_as_spam_spec.rb
+++ b/spec/requests/api/graphql/mutations/snippets/mark_as_spam_spec.rb
@@ -40,7 +40,7 @@ RSpec.describe 'Mark snippet as spam', feature_category: :source_code_management
let(:current_user) { other_user }
it_behaves_like 'a mutation that returns top-level errors',
- errors: [Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR]
+ errors: [Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR]
it_behaves_like 'does not mark the snippet as spam'
end
diff --git a/spec/requests/api/graphql/mutations/snippets/update_spec.rb b/spec/requests/api/graphql/mutations/snippets/update_spec.rb
index 78df78cb2a0..0bc475c7105 100644
--- a/spec/requests/api/graphql/mutations/snippets/update_spec.rb
+++ b/spec/requests/api/graphql/mutations/snippets/update_spec.rb
@@ -46,7 +46,7 @@ RSpec.describe 'Updating a Snippet', feature_category: :source_code_management d
let(:current_user) { create(:user) }
it_behaves_like 'a mutation that returns top-level errors',
- errors: [Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR]
+ errors: [Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR]
it 'does not update the Snippet' do
expect do
@@ -118,13 +118,15 @@ RSpec.describe 'Updating a Snippet', feature_category: :source_code_management d
describe 'PersonalSnippet' do
let(:snippet) do
- create(:personal_snippet,
- :private,
- :repository,
- file_name: original_file_name,
- title: original_title,
- content: original_content,
- description: original_description)
+ create(
+ :personal_snippet,
+ :private,
+ :repository,
+ file_name: original_file_name,
+ title: original_title,
+ content: original_content,
+ description: original_description
+ )
end
it_behaves_like 'graphql update actions'
@@ -139,15 +141,17 @@ RSpec.describe 'Updating a Snippet', feature_category: :source_code_management d
let_it_be(:project) { create(:project, :private) }
let(:snippet) do
- create(:project_snippet,
- :private,
- :repository,
- project: project,
- author: create(:user),
- file_name: original_file_name,
- title: original_title,
- content: original_content,
- description: original_description)
+ create(
+ :project_snippet,
+ :private,
+ :repository,
+ project: project,
+ author: create(:user),
+ file_name: original_file_name,
+ title: original_title,
+ content: original_content,
+ description: original_description
+ )
end
context 'when the author is not a member of the project' do
diff --git a/spec/requests/api/graphql/mutations/todos/mark_all_done_spec.rb b/spec/requests/api/graphql/mutations/todos/mark_all_done_spec.rb
index c611c6ee2a1..429aa06d9f1 100644
--- a/spec/requests/api/graphql/mutations/todos/mark_all_done_spec.rb
+++ b/spec/requests/api/graphql/mutations/todos/mark_all_done_spec.rb
@@ -21,12 +21,14 @@ RSpec.describe 'Marking all todos done', feature_category: :team_planning do
let(:input) { {} }
let(:mutation) do
- graphql_mutation(:todos_mark_all_done, input,
- <<-QL.strip_heredoc
- clientMutationId
- todos { id }
- errors
- QL
+ graphql_mutation(
+ :todos_mark_all_done,
+ input,
+ <<-QL.strip_heredoc
+ clientMutationId
+ todos { id }
+ errors
+ QL
)
end
diff --git a/spec/requests/api/graphql/mutations/todos/mark_done_spec.rb b/spec/requests/api/graphql/mutations/todos/mark_done_spec.rb
index 60700d8024c..c09f89ef567 100644
--- a/spec/requests/api/graphql/mutations/todos/mark_done_spec.rb
+++ b/spec/requests/api/graphql/mutations/todos/mark_done_spec.rb
@@ -19,15 +19,17 @@ RSpec.describe 'Marking todos done', feature_category: :team_planning do
let(:input) { { id: todo1.to_global_id.to_s } }
let(:mutation) do
- graphql_mutation(:todo_mark_done, input,
- <<-QL.strip_heredoc
- clientMutationId
- errors
- todo {
- id
- state
- }
- QL
+ graphql_mutation(
+ :todo_mark_done,
+ input,
+ <<-QL.strip_heredoc
+ clientMutationId
+ errors
+ todo {
+ id
+ state
+ }
+ QL
)
end
diff --git a/spec/requests/api/graphql/mutations/todos/restore_many_spec.rb b/spec/requests/api/graphql/mutations/todos/restore_many_spec.rb
index 9daa243cf8e..4bbfc7b2f1d 100644
--- a/spec/requests/api/graphql/mutations/todos/restore_many_spec.rb
+++ b/spec/requests/api/graphql/mutations/todos/restore_many_spec.rb
@@ -20,15 +20,17 @@ RSpec.describe 'Restoring many Todos', feature_category: :team_planning do
let(:input) { { ids: input_ids } }
let(:mutation) do
- graphql_mutation(:todo_restore_many, input,
- <<-QL.strip_heredoc
- clientMutationId
- errors
- todos {
- id
- state
- }
- QL
+ graphql_mutation(
+ :todo_restore_many,
+ input,
+ <<-QL.strip_heredoc
+ clientMutationId
+ errors
+ todos {
+ id
+ state
+ }
+ QL
)
end
diff --git a/spec/requests/api/graphql/mutations/todos/restore_spec.rb b/spec/requests/api/graphql/mutations/todos/restore_spec.rb
index 868298763ec..1ebd04432be 100644
--- a/spec/requests/api/graphql/mutations/todos/restore_spec.rb
+++ b/spec/requests/api/graphql/mutations/todos/restore_spec.rb
@@ -19,15 +19,17 @@ RSpec.describe 'Restoring Todos', feature_category: :team_planning do
let(:input) { { id: todo1.to_global_id.to_s } }
let(:mutation) do
- graphql_mutation(:todo_restore, input,
- <<-QL.strip_heredoc
- clientMutationId
- errors
- todo {
- id
- state
- }
- QL
+ graphql_mutation(
+ :todo_restore,
+ input,
+ <<-QL.strip_heredoc
+ clientMutationId
+ errors
+ todo {
+ id
+ state
+ }
+ QL
)
end
diff --git a/spec/requests/api/graphql/namespace/root_storage_statistics_spec.rb b/spec/requests/api/graphql/namespace/root_storage_statistics_spec.rb
index 7c48f324d24..c8819f1e38f 100644
--- a/spec/requests/api/graphql/namespace/root_storage_statistics_spec.rb
+++ b/spec/requests/api/graphql/namespace/root_storage_statistics_spec.rb
@@ -6,8 +6,15 @@ RSpec.describe 'rendering namespace statistics', feature_category: :metrics do
include GraphqlHelpers
let(:namespace) { user.namespace }
- let!(:statistics) { create(:namespace_root_storage_statistics, namespace: namespace, packages_size: 5.gigabytes, uploads_size: 3.gigabytes) }
let(:user) { create(:user) }
+ let!(:statistics) do
+ create(
+ :namespace_root_storage_statistics,
+ namespace: namespace,
+ packages_size: 5.gigabytes,
+ uploads_size: 3.gigabytes
+ )
+ end
let(:query) do
graphql_query_for(
diff --git a/spec/requests/api/graphql/organizations/organization_query_spec.rb b/spec/requests/api/graphql/organizations/organization_query_spec.rb
index d02158382eb..c243e0613ad 100644
--- a/spec/requests/api/graphql/organizations/organization_query_spec.rb
+++ b/spec/requests/api/graphql/organizations/organization_query_spec.rb
@@ -79,7 +79,10 @@ RSpec.describe 'getting organization information', feature_category: :cell do
<<~FIELDS
organizationUsers {
nodes {
- badges
+ badges {
+ text
+ variant
+ }
id
user {
id
@@ -94,7 +97,7 @@ RSpec.describe 'getting organization information', feature_category: :cell do
organization_user_node = graphql_data_at(:organization, :organizationUsers, :nodes).first
expected_attributes = {
- "badges" => ["It's you!"],
+ "badges" => [{ "text" => "It's you!", "variant" => 'muted' }],
"id" => organization_user.to_global_id.to_s,
"user" => { "id" => user.to_global_id.to_s }
}
diff --git a/spec/requests/api/graphql/project/base_service_spec.rb b/spec/requests/api/graphql/project/base_service_spec.rb
index b27cddea07b..646ff8dd8a8 100644
--- a/spec/requests/api/graphql/project/base_service_spec.rb
+++ b/spec/requests/api/graphql/project/base_service_spec.rb
@@ -41,7 +41,7 @@ RSpec.describe 'query Jira service', feature_category: :system_access do
it 'retuns list of jira imports' do
service_types = services.map { |s| s['type'] }
- expect(service_types).to match_array(%w(BugzillaService JiraService RedmineService))
+ expect(service_types).to match_array(%w[BugzillaService JiraService RedmineService])
end
end
end
diff --git a/spec/requests/api/graphql/project/container_repositories_spec.rb b/spec/requests/api/graphql/project/container_repositories_spec.rb
index 9a40a972256..c86d3bdd14c 100644
--- a/spec/requests/api/graphql/project/container_repositories_spec.rb
+++ b/spec/requests/api/graphql/project/container_repositories_spec.rb
@@ -46,7 +46,7 @@ RSpec.describe 'getting container repositories in a project', feature_category:
before do
stub_container_registry_config(enabled: true)
container_repositories.each do |repository|
- stub_container_registry_tags(repository: repository.path, tags: %w(tag1 tag2 tag3), with_manifest: false)
+ stub_container_registry_tags(repository: repository.path, tags: %w[tag1 tag2 tag3], with_manifest: false)
end
end
@@ -141,7 +141,7 @@ RSpec.describe 'getting container repositories in a project', feature_category:
end
before do
- stub_container_registry_tags(repository: container_repository.path, tags: %w(tag4 tag5 tag6), with_manifest: false)
+ stub_container_registry_tags(repository: container_repository.path, tags: %w[tag4 tag5 tag6], with_manifest: false)
end
it 'returns the searched container repository' do
@@ -175,11 +175,11 @@ RSpec.describe 'getting container repositories in a project', feature_category:
let_it_be(:container_repository5) { create(:container_repository, name: 'e', project: sort_project) }
before do
- stub_container_registry_tags(repository: container_repository1.path, tags: %w(tag1 tag1 tag3), with_manifest: false)
- stub_container_registry_tags(repository: container_repository2.path, tags: %w(tag4 tag5 tag6), with_manifest: false)
- stub_container_registry_tags(repository: container_repository3.path, tags: %w(tag7 tag8), with_manifest: false)
- stub_container_registry_tags(repository: container_repository4.path, tags: %w(tag9), with_manifest: false)
- stub_container_registry_tags(repository: container_repository5.path, tags: %w(tag10 tag11), with_manifest: false)
+ stub_container_registry_tags(repository: container_repository1.path, tags: %w[tag1 tag1 tag3], with_manifest: false)
+ stub_container_registry_tags(repository: container_repository2.path, tags: %w[tag4 tag5 tag6], with_manifest: false)
+ stub_container_registry_tags(repository: container_repository3.path, tags: %w[tag7 tag8], with_manifest: false)
+ stub_container_registry_tags(repository: container_repository4.path, tags: %w[tag9], with_manifest: false)
+ stub_container_registry_tags(repository: container_repository5.path, tags: %w[tag10 tag11], with_manifest: false)
end
def pagination_query(params)
diff --git a/spec/requests/api/graphql/project/data_transfer_spec.rb b/spec/requests/api/graphql/project/data_transfer_spec.rb
index aafa8d65eb9..79b2b10419b 100644
--- a/spec/requests/api/graphql/project/data_transfer_spec.rb
+++ b/spec/requests/api/graphql/project/data_transfer_spec.rb
@@ -68,45 +68,21 @@ RSpec.describe 'project data transfers', feature_category: :source_code_manageme
context 'when user has enough permissions' do
before do
project.add_owner(current_user)
+ subject
end
- context 'when data_transfer_monitoring_mock_data is NOT enabled' do
- before do
- stub_feature_flags(data_transfer_monitoring_mock_data: false)
- subject
- end
-
- it 'returns real results' do
- expect(response).to have_gitlab_http_status(:ok)
+ it 'returns real results' do
+ expect(response).to have_gitlab_http_status(:ok)
- expect(egress_data.count).to eq(2)
+ expect(egress_data.count).to eq(2)
- expect(egress_data.first.keys).to match_array(
- %w[date totalEgress repositoryEgress artifactsEgress packagesEgress registryEgress]
- )
+ expect(egress_data.first.keys).to match_array(
+ %w[date totalEgress repositoryEgress artifactsEgress packagesEgress registryEgress]
+ )
- expect(egress_data.pluck('repositoryEgress')).to match_array(%w[1 2])
- end
-
- it_behaves_like 'a working graphql query'
+ expect(egress_data.pluck('repositoryEgress')).to match_array(%w[1 2])
end
- context 'when data_transfer_monitoring_mock_data is enabled' do
- before do
- stub_feature_flags(data_transfer_monitoring_mock_data: true)
- subject
- end
-
- it 'returns mock results' do
- expect(response).to have_gitlab_http_status(:ok)
-
- expect(egress_data.count).to eq(12)
- expect(egress_data.first.keys).to match_array(
- %w[date totalEgress repositoryEgress artifactsEgress packagesEgress registryEgress]
- )
- end
-
- it_behaves_like 'a working graphql query'
- end
+ it_behaves_like 'a working graphql query'
end
end
diff --git a/spec/requests/api/graphql/project/environments_spec.rb b/spec/requests/api/graphql/project/environments_spec.rb
index 3a863bd3d77..94ce6b797cd 100644
--- a/spec/requests/api/graphql/project/environments_spec.rb
+++ b/spec/requests/api/graphql/project/environments_spec.rb
@@ -150,7 +150,7 @@ RSpec.describe 'Project Environments query', feature_category: :continuous_deliv
end
describe 'last deployments of environments' do
- ::Deployment.statuses.each do |status, _| # rubocop:disable RSpec/UselessDynamicDefinition
+ ::Deployment.statuses.each_key do |status| # rubocop:disable RSpec/UselessDynamicDefinition -- `status` used in `let_it_be`
let_it_be(:"production_#{status}_deployment") do
create(:deployment, status.to_sym, environment: production, project: project)
end
diff --git a/spec/requests/api/graphql/project/error_tracking/sentry_errors_request_spec.rb b/spec/requests/api/graphql/project/error_tracking/sentry_errors_request_spec.rb
index 6cbc70022ed..c2910938acf 100644
--- a/spec/requests/api/graphql/project/error_tracking/sentry_errors_request_spec.rb
+++ b/spec/requests/api/graphql/project/error_tracking/sentry_errors_request_spec.rb
@@ -119,7 +119,7 @@ RSpec.describe 'sentry errors requests', feature_category: :error_tracking do
end
let(:error_data) { graphql_data.dig('project', 'sentryErrors', 'errors', 'nodes') }
- let(:pagination_data) { graphql_data.dig('project', 'sentryErrors', 'errors', 'pageInfo') }
+ let(:pagination_data) { graphql_data.dig('project', 'sentryErrors', 'errors', 'pageInfo') }
it_behaves_like 'a working graphql query' do
before do
diff --git a/spec/requests/api/graphql/project/issue/design_collection/versions_spec.rb b/spec/requests/api/graphql/project/issue/design_collection/versions_spec.rb
index a15e4c1e792..bc56df5b6ff 100644
--- a/spec/requests/api/graphql/project/issue/design_collection/versions_spec.rb
+++ b/spec/requests/api/graphql/project/issue/design_collection/versions_spec.rb
@@ -73,7 +73,7 @@ RSpec.describe 'Getting versions related to an issue', feature_category: :design
post_graphql(query, current_user: current_user)
keys = graphql_data.dig(*edges_path).first['node'].keys
- expect(keys).to match_array(%w(id sha createdAt author))
+ expect(keys).to match_array(%w[id sha createdAt author])
end
end
diff --git a/spec/requests/api/graphql/project/issue_spec.rb b/spec/requests/api/graphql/project/issue_spec.rb
index bc90f9e89e6..8d21a9f0394 100644
--- a/spec/requests/api/graphql/project/issue_spec.rb
+++ b/spec/requests/api/graphql/project/issue_spec.rb
@@ -108,7 +108,7 @@ RSpec.describe 'Query.project(fullPath).issue(iid)', feature_category: :team_pla
let(:object_field_name) { :design }
let(:no_argument_error) do
- custom_graphql_error(path, a_string_matching(%r/id or filename/))
+ custom_graphql_error(path, a_string_matching(%r{id or filename}))
end
let_it_be(:object_on_other_issue) { create(:design, issue: issue_b) }
@@ -134,7 +134,7 @@ RSpec.describe 'Query.project(fullPath).issue(iid)', feature_category: :team_pla
it 'raises an error' do
post_query
- expect(graphql_errors).to include(custom_graphql_error(path, a_string_matching(%r/id or sha/)))
+ expect(graphql_errors).to include(custom_graphql_error(path, a_string_matching(%r{id or sha})))
end
end
diff --git a/spec/requests/api/graphql/project/jira_import_spec.rb b/spec/requests/api/graphql/project/jira_import_spec.rb
index 25cea0238ef..a1d340b3500 100644
--- a/spec/requests/api/graphql/project/jira_import_spec.rb
+++ b/spec/requests/api/graphql/project/jira_import_spec.rb
@@ -96,7 +96,7 @@ RSpec.describe 'query Jira import data', feature_category: :importers do
total_issue_count = jira_imports.map { |ji| ji.dig('totalIssueCount') }
expect(jira_imports.size).to eq 2
- expect(jira_proket_keys).to eq %w(BB AA)
+ expect(jira_proket_keys).to eq %w[BB AA]
expect(usernames).to eq [current_user.username, current_user.username]
expect(imported_issues_count).to eq [2, 2]
expect(failed_issues_count).to eq [1, 2]
diff --git a/spec/requests/api/graphql/project/jira_projects_spec.rb b/spec/requests/api/graphql/project/jira_projects_spec.rb
index 3cd689deda5..2859e8a7c99 100644
--- a/spec/requests/api/graphql/project/jira_projects_spec.rb
+++ b/spec/requests/api/graphql/project/jira_projects_spec.rb
@@ -55,8 +55,8 @@ RSpec.describe 'query Jira projects', feature_category: :integrations do
project_ids = jira_projects.map { |jp| jp['projectId'] }
expect(jira_projects.size).to eq(2)
- expect(project_keys).to eq(%w(EX ABC))
- expect(project_names).to eq(%w(Example Alphabetical))
+ expect(project_keys).to eq(%w[EX ABC])
+ expect(project_names).to eq(%w[Example Alphabetical])
expect(project_ids).to eq([10000, 10001])
end
@@ -69,8 +69,8 @@ RSpec.describe 'query Jira projects', feature_category: :integrations do
project_ids = jira_projects.map { |jp| jp['projectId'] }
expect(jira_projects.size).to eq(1)
- expect(project_keys).to eq(%w(EX))
- expect(project_names).to eq(%w(Example))
+ expect(project_keys).to eq(%w[EX])
+ expect(project_names).to eq(%w[Example])
expect(project_ids).to eq([10000])
end
end
diff --git a/spec/requests/api/graphql/project/merge_request_spec.rb b/spec/requests/api/graphql/project/merge_request_spec.rb
index c274199e65b..23be9fa5286 100644
--- a/spec/requests/api/graphql/project/merge_request_spec.rb
+++ b/spec/requests/api/graphql/project/merge_request_spec.rb
@@ -22,7 +22,7 @@ RSpec.describe 'getting merge request information nested in a project', feature_
it_behaves_like 'a working graphql query' do
# we exclude Project.pipeline because it needs arguments,
- # codequalityReportsComparer because no pipeline exist yet
+ # codequalityReportsComparer because it is behind a feature flag
# and runners because the user is not an admin and therefore has no access
let(:excluded) { %w[jobs pipeline runners codequalityReportsComparer] }
let(:mr_fields) { all_graphql_fields_for('MergeRequest', excluded: excluded) }
diff --git a/spec/requests/api/graphql/project/merge_requests_spec.rb b/spec/requests/api/graphql/project/merge_requests_spec.rb
index 543de43bcf3..176a02df0be 100644
--- a/spec/requests/api/graphql/project/merge_requests_spec.rb
+++ b/spec/requests/api/graphql/project/merge_requests_spec.rb
@@ -48,8 +48,11 @@ RSpec.describe 'getting merge request listings nested in a project', feature_cat
end
it_behaves_like 'a working graphql query' do
+ # we exclude codequalityReportsComparer because it is behind feature flag
+ let(:excluded) { %w[codequalityReportsComparer] }
+
let(:query) do
- query_merge_requests(all_graphql_fields_for('MergeRequest', max_depth: 2))
+ query_merge_requests(all_graphql_fields_for('MergeRequest', max_depth: 2, excluded: excluded))
end
before do
diff --git a/spec/requests/api/graphql/project/release_spec.rb b/spec/requests/api/graphql/project/release_spec.rb
index 8d4a39d6b30..8206d076d2e 100644
--- a/spec/requests/api/graphql/project/release_spec.rb
+++ b/spec/requests/api/graphql/project/release_spec.rb
@@ -36,7 +36,7 @@ RSpec.describe 'Query.project(fullPath).release(tagName)', feature_category: :re
let(:path) { path_prefix }
let(:release_fields) do
- %{
+ %(
tagName
tagPath
description
@@ -45,7 +45,7 @@ RSpec.describe 'Query.project(fullPath).release(tagName)', feature_category: :re
createdAt
releasedAt
upcomingRelease
- }
+ )
end
before do
@@ -176,14 +176,14 @@ RSpec.describe 'Query.project(fullPath).release(tagName)', feature_category: :re
let(:path) { path_prefix + %w[links] }
let(:release_fields) do
- query_graphql_field(:links, nil, %{
+ query_graphql_field(:links, nil, %(
selfUrl
openedMergeRequestsUrl
mergedMergeRequestsUrl
closedMergeRequestsUrl
openedIssuesUrl
closedIssuesUrl
- })
+ ))
end
it 'finds all release links' do
@@ -225,7 +225,7 @@ RSpec.describe 'Query.project(fullPath).release(tagName)', feature_category: :re
let(:path) { path_prefix }
let(:release_fields) do
- %{
+ %(
tagName
tagPath
description
@@ -234,7 +234,7 @@ RSpec.describe 'Query.project(fullPath).release(tagName)', feature_category: :re
createdAt
releasedAt
upcomingRelease
- }
+ )
end
before do
@@ -358,14 +358,14 @@ RSpec.describe 'Query.project(fullPath).release(tagName)', feature_category: :re
let(:path) { path_prefix + %w[links] }
let(:release_fields) do
- query_graphql_field(:links, nil, %{
+ query_graphql_field(:links, nil, %(
selfUrl
openedMergeRequestsUrl
mergedMergeRequestsUrl
closedMergeRequestsUrl
openedIssuesUrl
closedIssuesUrl
- })
+ ))
end
it 'finds only selfUrl' do
@@ -547,10 +547,10 @@ RSpec.describe 'Query.project(fullPath).release(tagName)', feature_category: :re
let(:current_user) { developer }
let(:release_fields) do
- %{
+ %(
releasedAt
upcomingRelease
- }
+ )
end
before do
@@ -588,13 +588,13 @@ RSpec.describe 'Query.project(fullPath).release(tagName)', feature_category: :re
let_it_be_with_reload(:release) { create(:release, project: project) }
let(:release_fields) do
- %{
+ %(
milestones {
nodes {
title
}
}
- }
+ )
end
let(:actual_milestone_title_order) do
diff --git a/spec/requests/api/graphql/project/terraform/state_spec.rb b/spec/requests/api/graphql/project/terraform/state_spec.rb
index 1889e7a1064..28f3868c7cf 100644
--- a/spec/requests/api/graphql/project/terraform/state_spec.rb
+++ b/spec/requests/api/graphql/project/terraform/state_spec.rb
@@ -20,7 +20,7 @@ RSpec.describe 'query a single terraform state', feature_category: :infrastructu
query_graphql_field(
:terraformState,
{ name: terraform_state.name },
- %{
+ %(
id
name
lockedAt
@@ -45,7 +45,7 @@ RSpec.describe 'query a single terraform state', feature_category: :infrastructu
lockedByUser {
id
}
- }
+ )
)
)
end
diff --git a/spec/requests/api/graphql/project/terraform/states_spec.rb b/spec/requests/api/graphql/project/terraform/states_spec.rb
index 7a789a5d481..d6cf3a52649 100644
--- a/spec/requests/api/graphql/project/terraform/states_spec.rb
+++ b/spec/requests/api/graphql/project/terraform/states_spec.rb
@@ -14,7 +14,7 @@ RSpec.describe 'query terraform states', feature_category: :infrastructure_as_co
graphql_query_for(
:project,
{ fullPath: project.full_path },
- %{
+ %(
terraformStates {
count
nodes {
@@ -45,7 +45,7 @@ RSpec.describe 'query terraform states', feature_category: :infrastructure_as_co
}
}
}
- }
+ )
)
end
diff --git a/spec/requests/api/graphql/project/work_items_spec.rb b/spec/requests/api/graphql/project/work_items_spec.rb
index d5d3d6c578f..d0f80bcfebe 100644
--- a/spec/requests/api/graphql/project/work_items_spec.rb
+++ b/spec/requests/api/graphql/project/work_items_spec.rb
@@ -350,7 +350,9 @@ RSpec.describe 'getting a work item list for a project', feature_category: :team
end
context 'when fetching work item linked items widget' do
- let_it_be(:related_items) { create_list(:work_item, 3, project: project, milestone: milestone1) }
+ let_it_be(:other_project) { create(:project, :repository, :public, group: group) }
+ let_it_be(:other_milestone) { create(:milestone, project: other_project) }
+ let_it_be(:related_items) { create_list(:work_item, 3, project: other_project, milestone: other_milestone) }
let(:fields) do
<<~GRAPHQL
@@ -384,21 +386,24 @@ RSpec.describe 'getting a work item list for a project', feature_category: :team
before do
create(:work_item_link, source: item1, target: related_items[0], link_type: 'relates_to')
+ create(:work_item_link, source: item2, target: related_items[0], link_type: 'relates_to')
end
it 'executes limited number of N+1 queries', :use_sql_query_cache do
+ post_graphql(query, current_user: current_user) # warm-up
+
control = ActiveRecord::QueryRecorder.new(skip_cached: false) do
post_graphql(query, current_user: current_user)
end
- create(:work_item_link, source: item1, target: related_items[1], link_type: 'relates_to')
- create(:work_item_link, source: item1, target: related_items[2], link_type: 'relates_to')
+ [item1, item2].each do |item|
+ create(:work_item_link, source: item, target: related_items[1], link_type: 'relates_to')
+ create(:work_item_link, source: item, target: related_items[2], link_type: 'relates_to')
+ end
expect_graphql_errors_to_be_empty
- # TODO: Fix N+1 queries executed for the linked work item widgets
- # https://gitlab.com/gitlab-org/gitlab/-/issues/420605
expect { post_graphql(query, current_user: current_user) }
- .not_to exceed_all_query_limit(control).with_threshold(11)
+ .not_to exceed_all_query_limit(control)
end
end
diff --git a/spec/requests/api/graphql/projects/projects_spec.rb b/spec/requests/api/graphql/projects/projects_spec.rb
new file mode 100644
index 00000000000..84b8c2285f0
--- /dev/null
+++ b/spec/requests/api/graphql/projects/projects_spec.rb
@@ -0,0 +1,77 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'getting a collection of projects', feature_category: :source_code_management do
+ include GraphqlHelpers
+
+ let_it_be(:current_user) { create(:user) }
+ let_it_be(:group) { create(:group, name: 'public-group') }
+ let_it_be(:projects) { create_list(:project, 5, :public, group: group) }
+ let_it_be(:other_project) { create(:project, :public, group: group) }
+
+ let(:filters) { {} }
+
+ let(:query) do
+ graphql_query_for(
+ :projects,
+ filters,
+ "nodes {#{all_graphql_fields_for('Project', max_depth: 1, excluded: ['productAnalyticsState'])} }"
+ )
+ end
+
+ before_all do
+ group.add_developer(current_user)
+ end
+
+ context 'when providing full_paths filter' do
+ let(:project_full_paths) { projects.map(&:full_path) }
+ let(:filters) { { full_paths: project_full_paths } }
+
+ let(:single_project_query) do
+ graphql_query_for(
+ :projects,
+ { full_paths: [project_full_paths.first] },
+ "nodes {#{all_graphql_fields_for('Project', max_depth: 1, excluded: ['productAnalyticsState'])} }"
+ )
+ end
+
+ it_behaves_like 'a working graphql query that returns data' do
+ before do
+ post_graphql(query, current_user: current_user)
+ end
+ end
+
+ it 'avoids N+1 queries', :use_sql_query_cache, :clean_gitlab_redis_cache do
+ post_graphql(single_project_query, current_user: current_user)
+
+ query_count = ActiveRecord::QueryRecorder.new do
+ post_graphql(single_project_query, current_user: current_user)
+ end.count
+
+ # There is an N+1 query for max_member_access_for_user_ids
+ expect do
+ post_graphql(query, current_user: current_user)
+ end.not_to exceed_all_query_limit(query_count + 5)
+ end
+
+ it 'returns the expected projects' do
+ post_graphql(query, current_user: current_user)
+ returned_full_paths = graphql_data_at(:projects, :nodes).pluck('fullPath')
+
+ expect(returned_full_paths).to match_array(project_full_paths)
+ end
+
+ context 'when users provides more than 50 full_paths' do
+ let(:filters) { { full_paths: Array.new(51) { other_project.full_path } } }
+
+ it 'returns an error' do
+ post_graphql(query, current_user: current_user)
+
+ expect(graphql_errors).to contain_exactly(
+ hash_including('message' => _('You cannot provide more than 50 full_paths'))
+ )
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/user_spec.rb b/spec/requests/api/graphql/user_spec.rb
index 41ee233dfc5..22ebc1be964 100644
--- a/spec/requests/api/graphql/user_spec.rb
+++ b/spec/requests/api/graphql/user_spec.rb
@@ -113,4 +113,36 @@ RSpec.describe 'User', feature_category: :user_profile do
end
end
end
+
+ describe 'organizations field' do
+ let_it_be(:organization_user) { create(:organization_user, user: current_user) }
+ let_it_be(:organization) { organization_user.organization }
+ let_it_be(:another_organization) { create(:organization) }
+ let_it_be(:another_user) { create(:user) }
+
+ let(:query) do
+ graphql_query_for(
+ :user,
+ { username: current_user.username.to_s.upcase },
+ 'organizations { nodes { path } }'
+ )
+ end
+
+ context 'with permission' do
+ it 'returns the relevant organization details' do
+ post_graphql(query, current_user: current_user)
+
+ expect(graphql_data.dig('user', 'organizations', 'nodes').pluck('path'))
+ .to match_array(organization.path)
+ end
+ end
+
+ context 'without permission' do
+ it 'does not return organization details' do
+ post_graphql(query, current_user: another_user)
+
+ expect(graphql_data.dig('user', 'organizations', 'nodes')).to be_nil
+ end
+ end
+ end
end
diff --git a/spec/requests/api/graphql/work_item_spec.rb b/spec/requests/api/graphql/work_item_spec.rb
index b8575b25e0a..36a27abd982 100644
--- a/spec/requests/api/graphql/work_item_spec.rb
+++ b/spec/requests/api/graphql/work_item_spec.rb
@@ -619,6 +619,47 @@ RSpec.describe 'Query.work_item(id)', feature_category: :team_planning do
)
end
+ context 'when inaccessible links are present' do
+ let_it_be(:no_access_item) { create(:work_item, title: "PRIVATE", project: create(:project, :private)) }
+
+ before do
+ create(:work_item_link, source: work_item, target: no_access_item, link_type: 'relates_to')
+ end
+
+ it 'returns only items that the user has access to' do
+ expect(graphql_dig_at(work_item_data, :widgets, "linkedItems", "nodes", "linkId"))
+ .to match_array([link1.to_gid.to_s, link2.to_gid.to_s])
+ end
+ end
+
+ context 'when limiting the number of results' do
+ it_behaves_like 'sorted paginated query' do
+ include_context 'no sort argument'
+
+ let(:first_param) { 1 }
+ let(:all_records) { [link1, link2] }
+ let(:data_path) { ['workItem', 'widgets', "linkedItems", -1] }
+
+ def widget_fields(args)
+ query_graphql_field(
+ :widgets, {}, query_graphql_field(
+ '... on WorkItemWidgetLinkedItems', {}, query_graphql_field(
+ 'linkedItems', args, "#{page_info} nodes { linkId }"
+ )
+ )
+ )
+ end
+
+ def pagination_query(params)
+ graphql_query_for('workItem', { 'id' => global_id }, widget_fields(params))
+ end
+
+ def pagination_results_data(nodes)
+ nodes.map { |item| GlobalID::Locator.locate(item['linkId']) }
+ end
+ end
+ end
+
context 'when filtering by link type' do
let(:work_item_fields) do
<<~GRAPHQL
@@ -664,20 +705,6 @@ RSpec.describe 'Query.work_item(id)', feature_category: :team_planning do
end
describe 'notes widget' do
- let(:work_item_fields) do
- <<~GRAPHQL
- id
- widgets {
- type
- ... on WorkItemWidgetNotes {
- system: discussions(filter: ONLY_ACTIVITY, first: 10) { nodes { id notes { nodes { id system internal body } } } },
- comments: discussions(filter: ONLY_COMMENTS, first: 10) { nodes { id notes { nodes { id system internal body } } } },
- all_notes: discussions(filter: ALL_NOTES, first: 10) { nodes { id notes { nodes { id system internal body } } } }
- }
- }
- GRAPHQL
- end
-
context 'when fetching award emoji from notes' do
let(:work_item_fields) do
<<~GRAPHQL
@@ -768,6 +795,26 @@ RSpec.describe 'Query.work_item(id)', feature_category: :team_planning do
expect { post_graphql(query, current_user: developer) }.not_to exceed_query_limit(control).with_threshold(4)
expect_graphql_errors_to_be_empty
end
+
+ context 'when work item is associated with a group' do
+ let_it_be(:group_work_item) { create(:work_item, :group_level, namespace: group) }
+ let_it_be(:group_work_item_note) { create(:note, noteable: group_work_item, author: developer, project: nil) }
+ let(:global_id) { group_work_item.to_gid.to_s }
+
+ before_all do
+ create(:award_emoji, awardable: group_work_item_note, name: 'rocket', user: developer)
+ end
+
+ it 'returns notes for the group work item' do
+ all_widgets = graphql_dig_at(work_item_data, :widgets)
+ notes_widget = all_widgets.find { |x| x['type'] == 'NOTES' }
+ notes = graphql_dig_at(notes_widget['discussions'], :nodes).flat_map { |d| d['notes']['nodes'] }
+
+ expect(notes).to contain_exactly(
+ hash_including('body' => group_work_item_note.note)
+ )
+ end
+ end
end
end
diff --git a/spec/requests/api/group_packages_spec.rb b/spec/requests/api/group_packages_spec.rb
index 0b4f6130132..0786815c787 100644
--- a/spec/requests/api/group_packages_spec.rb
+++ b/spec/requests/api/group_packages_spec.rb
@@ -137,6 +137,29 @@ RSpec.describe API::GroupPackages, feature_category: :package_registry do
it_behaves_like 'filters on each package_type', is_project: false
+ context 'filtering on package_version' do
+ include_context 'package filter context'
+
+ let!(:package1) { create(:nuget_package, project: project, version: '2.0.4') }
+ let!(:package2) { create(:nuget_package, project: project) }
+
+ it 'returns the versioned package' do
+ url = group_filter_url(:version, '2.0.4')
+ get api(url, user)
+
+ expect(json_response.length).to eq(1)
+ expect(json_response.first['version']).to eq(package1.version)
+ end
+
+ it 'include_versionless has no effect' do
+ url = "/groups/#{group.id}/packages?package_version=2.0.4&include_versionless=true"
+ get api(url, user)
+
+ expect(json_response.length).to eq(1)
+ expect(json_response.first['version']).to eq(package1.version)
+ end
+ end
+
context 'does not accept non supported package_type value' do
include_context 'package filter context'
diff --git a/spec/requests/api/groups_spec.rb b/spec/requests/api/groups_spec.rb
index 662e11f7cfb..327dfd0a76b 100644
--- a/spec/requests/api/groups_spec.rb
+++ b/spec/requests/api/groups_spec.rb
@@ -572,7 +572,8 @@ RSpec.describe API::Groups, feature_category: :groups_and_projects do
expect(json_response['require_two_factor_authentication']).to eq(group1.require_two_factor_authentication)
expect(json_response['two_factor_grace_period']).to eq(group1.two_factor_grace_period)
expect(json_response['auto_devops_enabled']).to eq(group1.auto_devops_enabled)
- expect(json_response['emails_disabled']).to eq(group1.emails_disabled)
+ expect(json_response['emails_disabled']).to eq(group1.emails_disabled?)
+ expect(json_response['emails_enabled']).to eq(group1.emails_enabled?)
expect(json_response['mentions_disabled']).to eq(group1.mentions_disabled)
expect(json_response['project_creation_level']).to eq('maintainer')
expect(json_response['subgroup_creation_level']).to eq('maintainer')
@@ -870,6 +871,38 @@ RSpec.describe API::Groups, feature_category: :groups_and_projects do
end
end
+ before do
+ stub_application_setting(update_namespace_name_rate_limit: 1)
+ end
+
+ it 'increments the update_namespace_name rate limit' do
+ put api("/groups/#{group1.id}", user1), params: { name: "#{new_group_name}_1" }
+
+ expect(::Gitlab::ApplicationRateLimiter.peek(:update_namespace_name, scope: group1)).to be_falsey
+
+ put api("/groups/#{group1.id}", user1), params: { name: "#{new_group_name}_2" }
+
+ expect(::Gitlab::ApplicationRateLimiter.peek(:update_namespace_name, scope: group1)).to be_truthy
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(group1.reload.name).to eq("#{new_group_name}_2")
+ end
+
+ context 'a name is not passed in' do
+ it 'does not mark name update throttling' do
+ expect(::Gitlab::ApplicationRateLimiter).not_to receive(:throttled?)
+
+ put api("/groups/#{group1.id}", user1), params: { path: 'another/path' }
+ end
+ end
+
+ context 'an empty name is passed in' do
+ it 'does not mark name update throttling' do
+ expect(::Gitlab::ApplicationRateLimiter).not_to receive(:throttled?)
+
+ put api("/groups/#{group1.id}", user1), params: { name: '' }
+ end
+ end
+
context 'when authenticated as the group owner' do
it 'updates the group', :aggregate_failures do
workhorse_form_with_file(
@@ -895,7 +928,8 @@ RSpec.describe API::Groups, feature_category: :groups_and_projects do
expect(json_response['require_two_factor_authentication']).to eq(false)
expect(json_response['two_factor_grace_period']).to eq(48)
expect(json_response['auto_devops_enabled']).to eq(nil)
- expect(json_response['emails_disabled']).to eq(nil)
+ expect(json_response['emails_disabled']).to eq(false)
+ expect(json_response['emails_enabled']).to eq(true)
expect(json_response['mentions_disabled']).to eq(nil)
expect(json_response['project_creation_level']).to eq("noone")
expect(json_response['subgroup_creation_level']).to eq("maintainer")
diff --git a/spec/requests/api/helm_packages_spec.rb b/spec/requests/api/helm_packages_spec.rb
index d6afd6f86ff..75f60c59759 100644
--- a/spec/requests/api/helm_packages_spec.rb
+++ b/spec/requests/api/helm_packages_spec.rb
@@ -101,6 +101,12 @@ RSpec.describe API::HelmPackages, feature_category: :package_registry do
end
it_behaves_like 'deploy token for package GET requests'
+
+ context 'when format param is not nil' do
+ let(:url) { "/projects/#{project.id}/packages/helm/stable/charts/#{package.name}-#{package.version}.tgz.prov" }
+
+ it_behaves_like 'rejects helm packages access', :maintainer, :not_found, '{"message":"404 Format prov Not Found"}'
+ end
end
describe 'POST /api/v4/projects/:id/packages/helm/api/:channel/charts/authorize' do
diff --git a/spec/requests/api/internal/base_spec.rb b/spec/requests/api/internal/base_spec.rb
index cf0cd9a2e85..e59633b6d35 100644
--- a/spec/requests/api/internal/base_spec.rb
+++ b/spec/requests/api/internal/base_spec.rb
@@ -111,12 +111,12 @@ RSpec.describe API::Internal::Base, feature_category: :system_access do
it 'returns new recovery codes when the user exists' do
allow_any_instance_of(User).to receive(:two_factor_enabled?).and_return(true)
allow_any_instance_of(User)
- .to receive(:generate_otp_backup_codes!).and_return(%w(119135e5a3ebce8e 34bd7b74adbc8861))
+ .to receive(:generate_otp_backup_codes!).and_return(%w[119135e5a3ebce8e 34bd7b74adbc8861])
subject
expect(json_response['success']).to be_truthy
- expect(json_response['recovery_codes']).to match_array(%w(119135e5a3ebce8e 34bd7b74adbc8861))
+ expect(json_response['recovery_codes']).to match_array(%w[119135e5a3ebce8e 34bd7b74adbc8861])
end
end
@@ -200,7 +200,7 @@ RSpec.describe API::Internal::Base, feature_category: :system_access do
params: {
key_id: key.id,
name: 'newtoken',
- scopes: %w(read_api badscope read_repository)
+ scopes: %w[read_api badscope read_repository]
},
headers: gitlab_shell_internal_api_request_header
@@ -216,14 +216,14 @@ RSpec.describe API::Internal::Base, feature_category: :system_access do
params: {
key_id: key.id,
name: 'newtoken',
- scopes: %w(read_api read_repository),
+ scopes: %w[read_api read_repository],
expires_at: max_pat_access_token_lifetime
},
headers: gitlab_shell_internal_api_request_header
expect(json_response['success']).to be_truthy
expect(json_response['token']).to match(/\A\S{#{token_size}}\z/)
- expect(json_response['scopes']).to match_array(%w(read_api read_repository))
+ expect(json_response['scopes']).to match_array(%w[read_api read_repository])
expect(json_response['expires_at']).to eq(max_pat_access_token_lifetime.iso8601)
end
end
@@ -236,14 +236,14 @@ RSpec.describe API::Internal::Base, feature_category: :system_access do
params: {
key_id: key.id,
name: 'newtoken',
- scopes: %w(read_api read_repository),
+ scopes: %w[read_api read_repository],
expires_at: 365.days.from_now
},
headers: gitlab_shell_internal_api_request_header
expect(json_response['success']).to be_truthy
expect(json_response['token']).to match(/\A\S{#{token_size}}\z/)
- expect(json_response['scopes']).to match_array(%w(read_api read_repository))
+ expect(json_response['scopes']).to match_array(%w[read_api read_repository])
expect(json_response['expires_at']).to eq(max_pat_access_token_lifetime.iso8601)
end
end
@@ -560,6 +560,20 @@ RSpec.describe API::Internal::Base, feature_category: :system_access do
end
end
+ context 'when Gitaly provides a relative_path argument', :request_store do
+ subject { push(key, project, relative_path: relative_path) }
+
+ let(:relative_path) { 'relative_path' }
+
+ it 'stores relative_path value in RequestStore' do
+ allow(Gitlab::SafeRequestStore).to receive(:[]=).and_call_original
+ expect(Gitlab::SafeRequestStore).to receive(:[]=).with(:gitlab_git_relative_path, relative_path)
+ subject
+
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+ end
+
context "git push with project.wiki" do
subject { push(key, project.wiki, env: env.to_json) }
@@ -744,6 +758,17 @@ RSpec.describe API::Internal::Base, feature_category: :system_access do
expect(json_response["gitaly"]["features"]).to eq('gitaly-feature-mep-mep' => 'false')
end
end
+
+ context 'with audit event' do
+ it 'does not send a git streaming audit event' do
+ expect(::Gitlab::Audit::Auditor).not_to receive(:audit)
+
+ pull(key, project)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response["need_audit"]).to be_falsy
+ end
+ end
end
context "git push" do
@@ -757,6 +782,7 @@ RSpec.describe API::Internal::Base, feature_category: :system_access do
expect(json_response["gl_project_path"]).to eq(project.full_path)
expect(json_response["gl_key_type"]).to eq("key")
expect(json_response["gl_key_id"]).to eq(key.id)
+ expect(json_response["need_audit"]).to be_falsy
expect(json_response["gitaly"]).not_to be_nil
expect(json_response["gitaly"]["repository"]).not_to be_nil
expect(json_response["gitaly"]["repository"]["storage_name"]).to eq(project.repository.gitaly_repository.storage_name)
@@ -885,7 +911,7 @@ RSpec.describe API::Internal::Base, feature_category: :system_access do
{
'action' => 'geo_proxy_to_primary',
'data' => {
- 'api_endpoints' => %w{geo/proxy_git_ssh/info_refs_receive_pack geo/proxy_git_ssh/receive_pack},
+ 'api_endpoints' => %w[geo/proxy_git_ssh/info_refs_receive_pack geo/proxy_git_ssh/receive_pack],
'gl_username' => 'testuser',
'primary_repo' => 'http://localhost:3000/testuser/repo.git'
}
@@ -1515,7 +1541,7 @@ RSpec.describe API::Internal::Base, feature_category: :system_access do
describe 'POST /internal/pre_receive' do
let(:valid_params) do
- { gl_repository: gl_repository }
+ { gl_repository: gl_repository, user_id: user.id }
end
it 'decreases the reference counter and returns the result' do
@@ -1527,6 +1553,12 @@ RSpec.describe API::Internal::Base, feature_category: :system_access do
expect(json_response['reference_counter_increased']).to be(true)
end
+
+ it 'sticks to the primary' do
+ expect(User.sticking).to receive(:find_caught_up_replica).with(:user, user.id)
+
+ post api("/internal/pre_receive"), params: valid_params, headers: gitlab_shell_internal_api_request_header
+ end
end
describe 'POST /internal/two_factor_config' do
diff --git a/spec/requests/api/internal/pages_spec.rb b/spec/requests/api/internal/pages_spec.rb
index 1eeb3404157..7e2a778f433 100644
--- a/spec/requests/api/internal/pages_spec.rb
+++ b/spec/requests/api/internal/pages_spec.rb
@@ -46,17 +46,7 @@ RSpec.describe API::Internal::Pages, feature_category: :pages do
end
end
- context 'when authenticated' do
- before do
- project.update_pages_deployment!(create(:pages_deployment, project: project))
- end
-
- around do |example|
- freeze_time do
- example.run
- end
- end
-
+ context 'when authenticated', :freeze_time do
context 'when domain does not exist' do
it 'responds with 204 no content' do
get api('/internal/pages'), headers: auth_header, params: { host: 'any-domain.gitlab.io' }
@@ -79,10 +69,6 @@ RSpec.describe API::Internal::Pages, feature_category: :pages do
end
context 'when there are no pages deployed for the related project' do
- before do
- project.mark_pages_as_not_deployed
- end
-
it 'responds with 204 No Content' do
get api('/internal/pages'), headers: auth_header, params: { host: 'pages.io' }
@@ -91,9 +77,7 @@ RSpec.describe API::Internal::Pages, feature_category: :pages do
end
context 'when there are pages deployed for the related project' do
- before do
- project.mark_pages_as_deployed
- end
+ let!(:deployment) { create(:pages_deployment, project: project) }
it 'domain lookup is case insensitive' do
get api('/internal/pages'), headers: auth_header, params: { host: 'Pages.IO' }
@@ -110,7 +94,6 @@ RSpec.describe API::Internal::Pages, feature_category: :pages do
expect(json_response['certificate']).to eq(pages_domain.certificate)
expect(json_response['key']).to eq(pages_domain.key)
- deployment = project.pages_metadatum.pages_deployment
expect(json_response['lookup_paths']).to eq(
[
{
@@ -144,10 +127,6 @@ RSpec.describe API::Internal::Pages, feature_category: :pages do
end
context 'when there are no pages deployed for the related project' do
- before do
- project.mark_pages_as_not_deployed
- end
-
it 'responds with 204 No Content' do
get api('/internal/pages'), headers: auth_header, params: { host: 'unique-domain.example.com' }
@@ -156,9 +135,7 @@ RSpec.describe API::Internal::Pages, feature_category: :pages do
end
context 'when there are pages deployed for the related project' do
- before do
- project.mark_pages_as_deployed
- end
+ let!(:deployment) { create(:pages_deployment, project: project) }
context 'when the unique domain is disabled' do
before do
@@ -186,7 +163,6 @@ RSpec.describe API::Internal::Pages, feature_category: :pages do
expect(response).to have_gitlab_http_status(:ok)
expect(response).to match_response_schema('internal/pages/virtual_domain')
- deployment = project.pages_metadatum.pages_deployment
expect(json_response['lookup_paths']).to eq(
[
{
@@ -218,10 +194,6 @@ RSpec.describe API::Internal::Pages, feature_category: :pages do
end
context 'when there are no pages deployed for the related project' do
- before do
- project.mark_pages_as_not_deployed
- end
-
it 'responds with 204 No Content' do
get api('/internal/pages'), headers: auth_header, params: { host: "#{group.path}.gitlab-pages.io" }
@@ -232,9 +204,7 @@ RSpec.describe API::Internal::Pages, feature_category: :pages do
end
context 'when there are pages deployed for the related project' do
- before do
- project.mark_pages_as_deployed
- end
+ let!(:deployment) { create(:pages_deployment, project: project) }
context 'with a regular project' do
it 'responds with the correct domain configuration' do
@@ -243,7 +213,6 @@ RSpec.describe API::Internal::Pages, feature_category: :pages do
expect(response).to have_gitlab_http_status(:ok)
expect(response).to match_response_schema('internal/pages/virtual_domain')
- deployment = project.pages_metadatum.pages_deployment
expect(json_response['lookup_paths']).to eq(
[
{
@@ -274,7 +243,7 @@ RSpec.describe API::Internal::Pages, feature_category: :pages do
3.times do
project = create(:project, group: group)
- project.mark_pages_as_deployed
+ create(:pages_deployment, project: project)
end
expect { get api('/internal/pages'), headers: auth_header, params: { host: "#{group.path}.gitlab-pages.io" } }
@@ -292,7 +261,6 @@ RSpec.describe API::Internal::Pages, feature_category: :pages do
expect(response).to have_gitlab_http_status(:ok)
expect(response).to match_response_schema('internal/pages/virtual_domain')
- deployment = project.pages_metadatum.pages_deployment
expect(json_response['lookup_paths']).to eq(
[
{
diff --git a/spec/requests/api/issues/get_group_issues_spec.rb b/spec/requests/api/issues/get_group_issues_spec.rb
index eaa3c46d0ca..0ae65479d5e 100644
--- a/spec/requests/api/issues/get_group_issues_spec.rb
+++ b/spec/requests/api/issues/get_group_issues_spec.rb
@@ -350,7 +350,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do
get api(base_url, admin)
expect(response).to have_gitlab_http_status(:ok)
- expect(json_response.last.keys).to include(*%w(id iid project_id title description))
+ expect(json_response.last.keys).to include(*%w[id iid project_id title description])
expect(json_response.last).not_to have_key('subscribed')
end
diff --git a/spec/requests/api/issues/get_project_issues_spec.rb b/spec/requests/api/issues/get_project_issues_spec.rb
index 137fba66eaa..9e54ec08486 100644
--- a/spec/requests/api/issues/get_project_issues_spec.rb
+++ b/spec/requests/api/issues/get_project_issues_spec.rb
@@ -530,7 +530,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do
get api("#{base_url}/issues", user)
expect(response).to have_gitlab_http_status(:ok)
- expect(json_response.last.keys).to include(*%w(id iid project_id title description))
+ expect(json_response.last.keys).to include(*%w[id iid project_id title description])
expect(json_response.last).not_to have_key('subscribed')
end
diff --git a/spec/requests/api/issues/issues_spec.rb b/spec/requests/api/issues/issues_spec.rb
index af289352778..ed71089c5a9 100644
--- a/spec/requests/api/issues/issues_spec.rb
+++ b/spec/requests/api/issues/issues_spec.rb
@@ -638,7 +638,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do
end
it 'returns an empty array if no issue matches labels with labels param as array' do
- get api('/issues', user), params: { labels: %w(foo bar) }
+ get api('/issues', user), params: { labels: %w[foo bar] }
expect_paginated_array_response([])
end
@@ -914,7 +914,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do
end
it 'fails to sort with non predefined options' do
- %w(milestone abracadabra).each do |sort_opt|
+ %w[milestone abracadabra].each do |sort_opt|
get api('/issues', user), params: { order_by: sort_opt, sort: 'asc' }
expect(response).to have_gitlab_http_status(:bad_request)
end
diff --git a/spec/requests/api/issues/post_projects_issues_spec.rb b/spec/requests/api/issues/post_projects_issues_spec.rb
index 1cd20680afb..60142e7e151 100644
--- a/spec/requests/api/issues/post_projects_issues_spec.rb
+++ b/spec/requests/api/issues/post_projects_issues_spec.rb
@@ -183,7 +183,7 @@ RSpec.describe API::Issues, :aggregate_failures, feature_category: :team_plannin
expect(response).to have_gitlab_http_status(:created)
expect(json_response['title']).to eq('new issue')
expect(json_response['description']).to be_nil
- expect(json_response['labels']).to eq(%w(label label2))
+ expect(json_response['labels']).to eq(%w[label label2])
expect(json_response['confidential']).to be_falsy
expect(json_response['assignee']['name']).to eq(user2.name)
expect(json_response['assignees'].first['name']).to eq(user2.name)
@@ -191,12 +191,12 @@ RSpec.describe API::Issues, :aggregate_failures, feature_category: :team_plannin
it 'creates a new project issue with labels param as array' do
post api("/projects/#{project.id}/issues", user),
- params: { title: 'new issue', labels: %w(label label2), weight: 3, assignee_ids: [user2.id] }
+ params: { title: 'new issue', labels: %w[label label2], weight: 3, assignee_ids: [user2.id] }
expect(response).to have_gitlab_http_status(:created)
expect(json_response['title']).to eq('new issue')
expect(json_response['description']).to be_nil
- expect(json_response['labels']).to eq(%w(label label2))
+ expect(json_response['labels']).to eq(%w[label label2])
expect(json_response['confidential']).to be_falsy
expect(json_response['assignee']['name']).to eq(user2.name)
expect(json_response['assignees'].first['name']).to eq(user2.name)
@@ -391,7 +391,7 @@ RSpec.describe API::Issues, :aggregate_failures, feature_category: :team_plannin
it 'cannot create new labels with labels param as array' do
expect do
- post api("/projects/#{project.id}/issues", non_member), params: { title: 'new issue', labels: %w(label label2) }
+ post api("/projects/#{project.id}/issues", non_member), params: { title: 'new issue', labels: %w[label label2] }
end.not_to change { project.labels.count }
end
end
diff --git a/spec/requests/api/issues/put_projects_issues_spec.rb b/spec/requests/api/issues/put_projects_issues_spec.rb
index dbba31cd4d6..070ef6057dd 100644
--- a/spec/requests/api/issues/put_projects_issues_spec.rb
+++ b/spec/requests/api/issues/put_projects_issues_spec.rb
@@ -359,7 +359,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do
it 'updates labels and touches the record with labels param as array', :aggregate_failures do
travel_to(2.minutes.from_now) do
- put api_for_user, params: { labels: %w(foo bar) }
+ put api_for_user, params: { labels: %w[foo bar] }
end
expect(response).to have_gitlab_http_status(:ok)
diff --git a/spec/requests/api/maven_packages_spec.rb b/spec/requests/api/maven_packages_spec.rb
index 1f841eefff2..578a4821b5e 100644
--- a/spec/requests/api/maven_packages_spec.rb
+++ b/spec/requests/api/maven_packages_spec.rb
@@ -946,6 +946,22 @@ RSpec.describe API::MavenPackages, feature_category: :package_registry do
expect(response).to have_gitlab_http_status(:forbidden)
end
+ context 'with basic auth' do
+ where(:token_type) do
+ %i[personal_access_token deploy_token job]
+ end
+
+ with_them do
+ let(:token) { send(token_type).token }
+
+ it "authorizes upload with #{params[:token_type]} token" do
+ authorize_upload({}, headers.merge(basic_auth_header(token_type == :job ? ::Gitlab::Auth::CI_JOB_USER : user.username, token)))
+
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+ end
+ end
+
def authorize_upload(params = {}, request_headers = headers)
put api("/projects/#{project.id}/packages/maven/com/example/my-app/#{version}/maven-metadata.xml/authorize"), params: params, headers: request_headers
end
@@ -1083,6 +1099,22 @@ RSpec.describe API::MavenPackages, feature_category: :package_registry do
expect(response).to have_gitlab_http_status(:forbidden)
end
+ context 'with basic auth' do
+ where(:token_type) do
+ %i[personal_access_token deploy_token job]
+ end
+
+ with_them do
+ let(:token) { send(token_type).token }
+
+ it "allows upload with #{params[:token_type]} token" do
+ upload_file(params: params, request_headers: headers.merge(basic_auth_header(token_type == :job ? ::Gitlab::Auth::CI_JOB_USER : user.username, token)))
+
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+ end
+ end
+
context 'file name is too long' do
let(:file_name) { 'a' * (Packages::Maven::FindOrCreatePackageService::MAX_FILE_NAME_LENGTH + 1) }
diff --git a/spec/requests/api/members_spec.rb b/spec/requests/api/members_spec.rb
index 8dab9d555cf..feb24a4e73f 100644
--- a/spec/requests/api/members_spec.rb
+++ b/spec/requests/api/members_spec.rb
@@ -826,48 +826,20 @@ RSpec.describe API::Members, feature_category: :groups_and_projects do
end
end
- context 'with admin_group_member FF disabled' do
- before do
- stub_feature_flags(admin_group_member: false)
- end
-
- it_behaves_like 'POST /:source_type/:id/members', 'project' do
- let(:source) { project }
- end
-
- it_behaves_like 'POST /:source_type/:id/members', 'group' do
- let(:source) { group }
- end
-
- it_behaves_like 'PUT /:source_type/:id/members/:user_id', 'project' do
- let(:source) { project }
- end
-
- it_behaves_like 'PUT /:source_type/:id/members/:user_id', 'group' do
- let(:source) { group }
- end
+ it_behaves_like 'POST /:source_type/:id/members', 'project' do
+ let(:source) { project }
end
- context 'with admin_group_member FF enabled' do
- before do
- stub_feature_flags(admin_group_member: true)
- end
-
- it_behaves_like 'POST /:source_type/:id/members', 'project' do
- let(:source) { project }
- end
-
- it_behaves_like 'POST /:source_type/:id/members', 'group' do
- let(:source) { group }
- end
+ it_behaves_like 'POST /:source_type/:id/members', 'group' do
+ let(:source) { group }
+ end
- it_behaves_like 'PUT /:source_type/:id/members/:user_id', 'project' do
- let(:source) { project }
- end
+ it_behaves_like 'PUT /:source_type/:id/members/:user_id', 'project' do
+ let(:source) { project }
+ end
- it_behaves_like 'PUT /:source_type/:id/members/:user_id', 'group' do
- let(:source) { group }
- end
+ it_behaves_like 'PUT /:source_type/:id/members/:user_id', 'group' do
+ let(:source) { group }
end
it_behaves_like 'DELETE /:source_type/:id/members/:user_id', 'project' do
diff --git a/spec/requests/api/merge_request_approvals_spec.rb b/spec/requests/api/merge_request_approvals_spec.rb
index a1d6abec97e..2de59750273 100644
--- a/spec/requests/api/merge_request_approvals_spec.rb
+++ b/spec/requests/api/merge_request_approvals_spec.rb
@@ -8,8 +8,6 @@ RSpec.describe API::MergeRequestApprovals, feature_category: :source_code_manage
let_it_be(:bot) { create(:user, :project_bot) }
let_it_be(:project) { create(:project, :public, :repository, creator: user, namespace: user.namespace) }
let_it_be(:approver) { create :user }
- let_it_be(:group) { create :group }
-
let(:merge_request) { create(:merge_request, :simple, author: user, source_project: project) }
describe 'GET :id/merge_requests/:merge_request_iid/approvals' do
@@ -87,6 +85,28 @@ RSpec.describe API::MergeRequestApprovals, feature_category: :source_code_manage
expect(response).to have_gitlab_http_status(:created)
end
+
+ it 'calls MergeRequests::UpdateReviewerStateService' do
+ unapprover = create(:user)
+
+ project.add_developer(approver)
+ project.add_developer(unapprover)
+ project.add_developer(create(:user))
+
+ create(:approval, user: approver, merge_request: merge_request)
+ create(:approval, user: unapprover, merge_request: merge_request)
+
+ expect_next_instance_of(
+ MergeRequests::UpdateReviewerStateService,
+ project: project, current_user: unapprover
+ ) do |service|
+ expect(service).to receive(:execute).with(merge_request, "unreviewed")
+ end
+
+ post api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/unapprove", unapprover)
+
+ expect(response).to have_gitlab_http_status(:created)
+ end
end
end
diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb
index 2cf8872cd40..6000fa29dc4 100644
--- a/spec/requests/api/merge_requests_spec.rb
+++ b/spec/requests/api/merge_requests_spec.rb
@@ -229,7 +229,7 @@ RSpec.describe API::MergeRequests, :aggregate_failures, feature_category: :sourc
merge_request_closed.id,
merge_request.id
])
- expect(json_response.last.keys).to match_array(%w(id iid title web_url created_at description project_id state updated_at))
+ expect(json_response.last.keys).to match_array(%w[id iid title web_url created_at description project_id state updated_at])
expect(json_response.last['iid']).to eq(merge_request.iid)
expect(json_response.last['title']).to eq(merge_request.title)
expect(json_response.last).to have_key('web_url')
@@ -2175,7 +2175,7 @@ RSpec.describe API::MergeRequests, :aggregate_failures, feature_category: :sourc
expect(response).to have_gitlab_http_status(:created)
expect(json_response['title']).to eq('Test merge_request')
- expect(json_response['labels']).to eq(%w(label label2))
+ expect(json_response['labels']).to eq(%w[label label2])
expect(json_response['milestone']['id']).to eq(milestone.id)
expect(json_response['squash']).to be_truthy
expect(json_response['force_remove_source_branch']).to be_falsy
@@ -2187,11 +2187,11 @@ RSpec.describe API::MergeRequests, :aggregate_failures, feature_category: :sourc
end
it_behaves_like 'creates merge request with labels' do
- let(:labels) { %w(label label2) }
+ let(:labels) { %w[label label2] }
end
it_behaves_like 'creates merge request with labels' do
- let(:labels) { %w(label label2) }
+ let(:labels) { %w[label label2] }
end
it 'creates merge request with special label names' do
diff --git a/spec/requests/api/metadata_spec.rb b/spec/requests/api/metadata_spec.rb
index b81fe3f51b5..c8cee31db47 100644
--- a/spec/requests/api/metadata_spec.rb
+++ b/spec/requests/api/metadata_spec.rb
@@ -26,7 +26,7 @@ RSpec.describe API::Metadata, feature_category: :shared do
let(:personal_access_token) { create(:personal_access_token, scopes: scopes) }
context 'with api scope' do
- let(:scopes) { %i(api) }
+ let(:scopes) { %i[api] }
it 'returns the metadata information' do
get api(endpoint, personal_access_token: personal_access_token)
@@ -42,7 +42,7 @@ RSpec.describe API::Metadata, feature_category: :shared do
end
context 'with ai_features scope' do
- let(:scopes) { %i(ai_features) }
+ let(:scopes) { %i[ai_features] }
it 'returns the metadata information' do
get api(endpoint, personal_access_token: personal_access_token)
@@ -58,7 +58,7 @@ RSpec.describe API::Metadata, feature_category: :shared do
end
context 'with read_user scope' do
- let(:scopes) { %i(read_user) }
+ let(:scopes) { %i[read_user] }
it 'returns the metadata information' do
get api(endpoint, personal_access_token: personal_access_token)
@@ -74,7 +74,7 @@ RSpec.describe API::Metadata, feature_category: :shared do
end
context 'with neither api, ai_features nor read_user scope' do
- let(:scopes) { %i(read_repository) }
+ let(:scopes) { %i[read_repository] }
it 'returns authorization error' do
get api(endpoint, personal_access_token: personal_access_token)
diff --git a/spec/requests/api/ml/mlflow/model_versions_spec.rb b/spec/requests/api/ml/mlflow/model_versions_spec.rb
new file mode 100644
index 00000000000..f59888ec70f
--- /dev/null
+++ b/spec/requests/api/ml/mlflow/model_versions_spec.rb
@@ -0,0 +1,86 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe API::Ml::Mlflow::ModelVersions, feature_category: :mlops do
+ let_it_be(:project) { create(:project) }
+ let_it_be(:developer) { create(:user).tap { |u| project.add_developer(u) } }
+ let_it_be(:another_project) { build(:project).tap { |p| p.add_developer(developer) } }
+
+ let_it_be(:name) { 'a-model-name' }
+ let_it_be(:version) { '0.0.1' }
+ let_it_be(:model) { create(:ml_models, project: project, name: name) }
+ let_it_be(:model_version) { create(:ml_model_versions, project: project, model: model, version: version) }
+
+ let_it_be(:tokens) do
+ {
+ write: create(:personal_access_token, scopes: %w[read_api api], user: developer),
+ read: create(:personal_access_token, scopes: %w[read_api], user: developer),
+ no_access: create(:personal_access_token, scopes: %w[read_user], user: developer),
+ different_user: create(:personal_access_token, scopes: %w[read_api api], user: build(:user))
+ }
+ end
+
+ let(:current_user) { developer }
+ let(:access_token) { tokens[:write] }
+ let(:headers) { { 'Authorization' => "Bearer #{access_token.token}" } }
+ let(:project_id) { project.id }
+ let(:default_params) { {} }
+ let(:params) { default_params }
+ let(:request) { get api(route), params: params, headers: headers }
+ let(:json_response) { Gitlab::Json.parse(api_response.body) }
+
+ subject(:api_response) do
+ request
+ response
+ end
+
+ describe 'GET /projects/:id/ml/mlflow/api/2.0/mlflow/model_versions/get' do
+ let(:route) do
+ "/projects/#{project_id}/ml/mlflow/api/2.0/mlflow/model_versions/get?name=#{name}&version=#{version}"
+ end
+
+ it 'returns the model version', :aggregate_failures do
+ is_expected.to have_gitlab_http_status(:ok)
+ expect(json_response['model_version']).not_to be_nil
+ expect(json_response['model_version']['name']).to eq(name)
+ expect(json_response['model_version']['version']).to eq(version)
+ end
+
+ describe 'Error States' do
+ context 'when has access' do
+ context 'and model name in incorrect' do
+ let(:route) do
+ "/projects/#{project_id}/ml/mlflow/api/2.0/mlflow/model_versions/get?name=--&version=#{version}"
+ end
+
+ it_behaves_like 'MLflow|Not Found - Resource Does Not Exist'
+ end
+
+ context 'and version in incorrect' do
+ let(:route) do
+ "/projects/#{project_id}/ml/mlflow/api/2.0/mlflow/model_versions/get?name=#{name}&version=--"
+ end
+
+ it_behaves_like 'MLflow|Not Found - Resource Does Not Exist'
+ end
+
+ context 'when user lacks read_model_registry rights' do
+ before do
+ allow(Ability).to receive(:allowed?).and_call_original
+ allow(Ability).to receive(:allowed?)
+ .with(current_user, :read_model_registry, project)
+ .and_return(false)
+ end
+
+ it "is Not Found" do
+ is_expected.to have_gitlab_http_status(:not_found)
+ end
+ end
+ end
+
+ it_behaves_like 'MLflow|shared model registry error cases'
+ it_behaves_like 'MLflow|Requires read_api scope'
+ end
+ end
+end
diff --git a/spec/requests/api/ml/mlflow/registered_models_spec.rb b/spec/requests/api/ml/mlflow/registered_models_spec.rb
new file mode 100644
index 00000000000..cd8b0a53ef3
--- /dev/null
+++ b/spec/requests/api/ml/mlflow/registered_models_spec.rb
@@ -0,0 +1,203 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe API::Ml::Mlflow::RegisteredModels, feature_category: :mlops do
+ let_it_be(:project) { create(:project) }
+ let_it_be(:developer) { create(:user).tap { |u| project.add_developer(u) } }
+ let_it_be(:model) do
+ create(:ml_models, :with_metadata, project: project)
+ end
+
+ let_it_be(:tokens) do
+ {
+ write: create(:personal_access_token, scopes: %w[read_api api], user: developer),
+ read: create(:personal_access_token, scopes: %w[read_api], user: developer),
+ no_access: create(:personal_access_token, scopes: %w[read_user], user: developer),
+ different_user: create(:personal_access_token, scopes: %w[read_api api], user: build(:user))
+ }
+ end
+
+ let(:current_user) { developer }
+ let(:access_token) { tokens[:write] }
+ let(:headers) { { 'Authorization' => "Bearer #{access_token.token}" } }
+ let(:project_id) { project.id }
+ let(:default_params) { {} }
+ let(:params) { default_params }
+ let(:request) { get api(route), params: params, headers: headers }
+ let(:json_response) { Gitlab::Json.parse(api_response.body) }
+
+ subject(:api_response) do
+ request
+ response
+ end
+
+ describe 'GET /projects/:id/ml/mlflow/api/2.0/mlflow/registered-models/get' do
+ let(:model_name) { model.name }
+ let(:route) { "/projects/#{project_id}/ml/mlflow/api/2.0/mlflow/registered-models/get?name=#{model_name}" }
+
+ it 'returns the model', :aggregate_failures do
+ is_expected.to have_gitlab_http_status(:ok)
+ is_expected.to match_response_schema('ml/get_model')
+ end
+
+ describe 'Error States' do
+ context 'when has access' do
+ context 'and model does not exist' do
+ let(:model_name) { 'foo' }
+
+ it_behaves_like 'MLflow|Not Found - Resource Does Not Exist'
+ end
+
+ context 'and name is not passed' do
+ let(:route) { "/projects/#{project_id}/ml/mlflow/api/2.0/mlflow/registered-models/get" }
+
+ it_behaves_like 'MLflow|Not Found - Resource Does Not Exist'
+ end
+ end
+
+ it_behaves_like 'MLflow|shared model registry error cases'
+ it_behaves_like 'MLflow|Requires read_api scope'
+ end
+ end
+
+ describe 'POST /projects/:id/ml/mlflow/api/2.0/mlflow/registered-models/create' do
+ let(:route) do
+ "/projects/#{project_id}/ml/mlflow/api/2.0/mlflow/registered-models/create"
+ end
+
+ let(:params) { { name: 'my-model-name' } }
+ let(:request) { post api(route), params: params, headers: headers }
+
+ it 'creates the model', :aggregate_failures do
+ is_expected.to have_gitlab_http_status(:ok)
+ expect(json_response).to include('registered_model')
+ end
+
+ describe 'Error States' do
+ context 'when the model name is not passed' do
+ let(:params) { {} }
+
+ it_behaves_like 'MLflow|Bad Request'
+ end
+
+ context 'when the model name already exists' do
+ let(:existing_model) do
+ create(:ml_models, user: current_user, project: project)
+ end
+
+ let(:params) { { name: existing_model.name } }
+
+ it "is Bad Request", :aggregate_failures do
+ is_expected.to have_gitlab_http_status(:bad_request)
+
+ expect(json_response).to include({ 'error_code' => 'RESOURCE_ALREADY_EXISTS' })
+ end
+ end
+
+ context 'when project does not exist' do
+ let(:route) { "/projects/#{non_existing_record_id}/ml/mlflow/api/2.0/mlflow/registered-models/create" }
+
+ it "is Not Found", :aggregate_failures do
+ is_expected.to have_gitlab_http_status(:not_found)
+
+ expect(json_response['message']).to eq('404 Project Not Found')
+ end
+ end
+
+ # TODO: Ensure consisted error responses https://gitlab.com/gitlab-org/gitlab/-/issues/429731
+ context 'when a duplicate tag name is supplied' do
+ let(:params) do
+ { name: 'my-model-name', tags: [{ key: 'key1', value: 'value1' }, { key: 'key1', value: 'value2' }] }
+ end
+
+ it "creates the model with only the second tag", :aggregate_failures do
+ expect(json_response).to include({ 'error_code' => 'RESOURCE_ALREADY_EXISTS' })
+ end
+ end
+
+ # TODO: Ensure consisted error responses https://gitlab.com/gitlab-org/gitlab/-/issues/429731
+ context 'when an empty tag name is supplied' do
+ let(:params) do
+ { name: 'my-model-name', tags: [{ key: '', value: 'value1' }, { key: 'key1', value: 'value2' }] }
+ end
+
+ it "creates the model with only the second tag", :aggregate_failures do
+ expect(json_response).to include({ 'error_code' => 'RESOURCE_ALREADY_EXISTS' })
+ end
+ end
+
+ it_behaves_like 'MLflow|shared model registry error cases'
+ it_behaves_like 'MLflow|Requires api scope and write permission'
+ end
+ end
+
+ describe 'PATCH /projects/:id/ml/mlflow/api/2.0/mlflow/registered-models/update' do
+ let(:model_name) { model.name }
+ let(:model_description) { 'updated model description' }
+ let(:params) { { name: model_name, description: model_description } }
+ let(:route) { "/projects/#{project_id}/ml/mlflow/api/2.0/mlflow/registered-models/update" }
+ let(:request) { patch api(route), params: params, headers: headers }
+
+ it 'returns the updated model', :aggregate_failures do
+ is_expected.to have_gitlab_http_status(:ok)
+ is_expected.to match_response_schema('ml/update_model')
+ expect(json_response["registered_model"]["description"]).to eq(model_description)
+ end
+
+ describe 'Error States' do
+ context 'when has access' do
+ context 'and model does not exist' do
+ let(:model_name) { 'foo' }
+
+ it_behaves_like 'MLflow|Not Found - Resource Does Not Exist'
+ end
+
+ context 'and name is not passed' do
+ let(:params) { { description: model_description } }
+
+ it_behaves_like 'MLflow|Not Found - Resource Does Not Exist'
+ end
+ end
+
+ it_behaves_like 'MLflow|shared model registry error cases'
+ it_behaves_like 'MLflow|Requires api scope and write permission'
+ end
+ end
+
+ describe 'POST /projects/:id/ml/mlflow/api/2.0/mlflow/registered-models/get-latest-versions' do
+ let_it_be(:version1) { create(:ml_model_versions, model: model, created_at: 1.week.ago) }
+ let_it_be(:version2) { create(:ml_model_versions, model: model, created_at: 1.day.ago) }
+
+ let(:model_name) { model.name }
+ let(:params) { { name: model_name } }
+ let(:route) { "/projects/#{project_id}/ml/mlflow/api/2.0/mlflow/registered-models/get-latest-versions" }
+ let(:request) { post api(route), params: params, headers: headers }
+
+ it 'returns an array with the most recently created model version', :aggregate_failures do
+ is_expected.to have_gitlab_http_status(:ok)
+ is_expected.to match_response_schema('ml/get_latest_versions')
+ expect(json_response["model_versions"][0]["name"]).to eq(model_name)
+ expect(json_response["model_versions"][0]["version"]).to eq(version2.version)
+ end
+
+ describe 'Error States' do
+ context 'when has access' do
+ context 'and model does not exist' do
+ let(:model_name) { 'foo' }
+
+ it_behaves_like 'MLflow|Not Found - Resource Does Not Exist'
+ end
+
+ context 'and name is not passed' do
+ let(:params) { {} }
+
+ it_behaves_like 'MLflow|Not Found - Resource Does Not Exist'
+ end
+ end
+
+ it_behaves_like 'MLflow|shared model registry error cases'
+ it_behaves_like 'MLflow|Requires read_api scope'
+ end
+ end
+end
diff --git a/spec/requests/api/npm_project_packages_spec.rb b/spec/requests/api/npm_project_packages_spec.rb
index 340420e46e0..b5f38698857 100644
--- a/spec/requests/api/npm_project_packages_spec.rb
+++ b/spec/requests/api/npm_project_packages_spec.rb
@@ -196,7 +196,7 @@ RSpec.describe API::NpmProjectPackages, feature_category: :package_registry do
context 'with a job token for a different user' do
let_it_be(:other_user) { create(:user) }
- let_it_be_with_reload(:other_job) { create(:ci_build, :running, user: other_user) }
+ let_it_be_with_reload(:other_job) { create(:ci_build, :running, user: other_user, project: project) }
let(:headers) { build_token_auth_header(other_job.token) }
@@ -245,7 +245,7 @@ RSpec.describe API::NpmProjectPackages, feature_category: :package_registry do
context 'with a job token for a different user' do
let_it_be(:other_user) { create(:user) }
- let_it_be_with_reload(:other_job) { create(:ci_build, :running, user: other_user) }
+ let_it_be_with_reload(:other_job) { create(:ci_build, :running, user: other_user, project: project) }
let(:headers) { build_token_auth_header(other_job.token) }
diff --git a/spec/requests/api/pages/pages_spec.rb b/spec/requests/api/pages/pages_spec.rb
index aa1869eaa84..74dd1ed4f2b 100644
--- a/spec/requests/api/pages/pages_spec.rb
+++ b/spec/requests/api/pages/pages_spec.rb
@@ -9,7 +9,6 @@ RSpec.describe API::Pages, feature_category: :pages do
before do
project.add_maintainer(user)
- project.mark_pages_as_deployed
end
describe 'DELETE /projects/:id/pages' do
@@ -17,7 +16,7 @@ RSpec.describe API::Pages, feature_category: :pages do
it_behaves_like 'DELETE request permissions for admin mode' do
before do
- allow(Gitlab.config.pages).to receive(:enabled).and_return(true)
+ stub_pages_setting(enabled: true)
end
let(:succes_status_code) { :no_content }
@@ -25,7 +24,7 @@ RSpec.describe API::Pages, feature_category: :pages do
context 'when Pages is disabled' do
before do
- allow(Gitlab.config.pages).to receive(:enabled).and_return(false)
+ stub_pages_setting(enabled: false)
end
it_behaves_like '404 response' do
@@ -35,7 +34,7 @@ RSpec.describe API::Pages, feature_category: :pages do
context 'when Pages is enabled' do
before do
- allow(Gitlab.config.pages).to receive(:enabled).and_return(true)
+ stub_pages_setting(enabled: true)
end
context 'when Pages are deployed' do
@@ -48,15 +47,11 @@ RSpec.describe API::Pages, feature_category: :pages do
it 'removes the pages' do
delete api(path, admin, admin_mode: true)
- expect(project.reload.pages_metadatum.deployed?).to be(false)
+ expect(project.reload.pages_deployed?).to be(false)
end
end
context 'when pages are not deployed' do
- before do
- project.mark_pages_as_not_deployed
- end
-
it 'returns 204' do
delete api(path, admin, admin_mode: true)
diff --git a/spec/requests/api/personal_access_tokens_spec.rb b/spec/requests/api/personal_access_tokens_spec.rb
index 166768ea605..a1d29c4a935 100644
--- a/spec/requests/api/personal_access_tokens_spec.rb
+++ b/spec/requests/api/personal_access_tokens_spec.rb
@@ -461,6 +461,18 @@ RSpec.describe API::PersonalAccessTokens, :aggregate_failures, feature_category:
expect(json_response['expires_at']).to eq((Date.today + 1.week).to_s)
end
+ context 'when expiry is defined' do
+ it "rotates user's own token", :freeze_time do
+ expiry_date = Date.today + 1.month
+
+ post(api(path, token.user), params: { expires_at: expiry_date })
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response['token']).not_to eq(token.token)
+ expect(json_response['expires_at']).to eq(expiry_date.to_s)
+ end
+ end
+
context 'without permission' do
it 'returns an error message' do
another_user = create(:user)
diff --git a/spec/requests/api/project_attributes.yml b/spec/requests/api/project_attributes.yml
index ec98df22af7..165ea7bf66e 100644
--- a/spec/requests/api/project_attributes.yml
+++ b/spec/requests/api/project_attributes.yml
@@ -98,6 +98,7 @@ ci_cd_settings:
- merge_pipelines_enabled
- auto_rollback_enabled
- inbound_job_token_scope_enabled
+ - restrict_pipeline_cancellation_role
remapped_attributes:
default_git_depth: ci_default_git_depth
forward_deployment_enabled: ci_forward_deployment_enabled
diff --git a/spec/requests/api/project_container_repositories_spec.rb b/spec/requests/api/project_container_repositories_spec.rb
index f51b94bb78e..7797e8e9402 100644
--- a/spec/requests/api/project_container_repositories_spec.rb
+++ b/spec/requests/api/project_container_repositories_spec.rb
@@ -168,7 +168,7 @@ RSpec.describe API::ProjectContainerRepositories, feature_category: :container_r
let(:api_user) { reporter }
before do
- stub_container_registry_tags(repository: root_repository.path, tags: %w(rootA latest))
+ stub_container_registry_tags(repository: root_repository.path, tags: %w[rootA latest])
end
it_behaves_like 'a package tracking event', described_class.name, 'list_tags'
@@ -177,7 +177,7 @@ RSpec.describe API::ProjectContainerRepositories, feature_category: :container_r
subject
expect(json_response.length).to eq(2)
- expect(json_response.map { |repository| repository['name'] }).to eq %w(latest rootA)
+ expect(json_response.map { |repository| repository['name'] }).to eq %w[latest rootA]
end
it 'returns a matching schema' do
@@ -362,7 +362,7 @@ RSpec.describe API::ProjectContainerRepositories, feature_category: :container_r
let(:api_user) { reporter }
before do
- stub_container_registry_tags(repository: root_repository.path, tags: %w(rootA), with_manifest: true)
+ stub_container_registry_tags(repository: root_repository.path, tags: %w[rootA], with_manifest: true)
end
it 'returns a details of tag' do
@@ -408,7 +408,7 @@ RSpec.describe API::ProjectContainerRepositories, feature_category: :container_r
context 'when there are multiple tags' do
before do
- stub_container_registry_tags(repository: root_repository.path, tags: %w(rootA rootB), with_manifest: true)
+ stub_container_registry_tags(repository: root_repository.path, tags: %w[rootA rootB], with_manifest: true)
end
it 'properly removes tag' do
@@ -427,7 +427,7 @@ RSpec.describe API::ProjectContainerRepositories, feature_category: :container_r
context 'when there\'s only one tag' do
before do
- stub_container_registry_tags(repository: root_repository.path, tags: %w(rootA), with_manifest: true)
+ stub_container_registry_tags(repository: root_repository.path, tags: %w[rootA], with_manifest: true)
end
it 'properly removes tag' do
diff --git a/spec/requests/api/project_packages_spec.rb b/spec/requests/api/project_packages_spec.rb
index 219c748c9a6..2ac9a7d97f1 100644
--- a/spec/requests/api/project_packages_spec.rb
+++ b/spec/requests/api/project_packages_spec.rb
@@ -197,6 +197,26 @@ RSpec.describe API::ProjectPackages, feature_category: :package_registry do
end
end
+ context 'filtering on package_version' do
+ include_context 'package filter context'
+
+ it 'returns the versioned package' do
+ url = package_filter_url(:version, '2.0.4')
+ get api(url, user)
+
+ expect(json_response.length).to eq(1)
+ expect(json_response.first['version']).to eq(package2.version)
+ end
+
+ it 'include_versionless has no effect' do
+ url = "/projects/#{project.id}/packages?package_version=2.0.4&include_versionless=true"
+ get api(url, user)
+
+ expect(json_response.length).to eq(1)
+ expect(json_response.first['version']).to eq(package2.version)
+ end
+ end
+
it_behaves_like 'with versionless packages'
it_behaves_like 'with status param'
it_behaves_like 'does not cause n^2 queries'
diff --git a/spec/requests/api/project_repository_storage_moves_spec.rb b/spec/requests/api/project_repository_storage_moves_spec.rb
index 96ed3042d00..7b5dc0d5ef8 100644
--- a/spec/requests/api/project_repository_storage_moves_spec.rb
+++ b/spec/requests/api/project_repository_storage_moves_spec.rb
@@ -8,5 +8,29 @@ RSpec.describe API::ProjectRepositoryStorageMoves, feature_category: :gitaly do
let_it_be(:storage_move) { create(:project_repository_storage_move, :scheduled, container: container) }
let(:repository_storage_move_factory) { :project_repository_storage_move }
let(:bulk_worker_klass) { Projects::ScheduleBulkRepositoryShardMovesWorker }
+
+ context 'when project is hidden' do
+ let_it_be(:container) { create(:project, :hidden) }
+ let_it_be(:storage_move) { create(:project_repository_storage_move, :scheduled, container: container) }
+
+ it_behaves_like 'get single container repository storage move' do
+ let(:container_id) { container.id }
+ let(:url) { "/projects/#{container_id}/repository_storage_moves/#{repository_storage_move_id}" }
+ end
+
+ it_behaves_like 'post single container repository storage move'
+ end
+
+ context 'when project is pending delete' do
+ let_it_be(:container) { create(:project, pending_delete: true) }
+ let_it_be(:storage_move) { create(:project_repository_storage_move, :scheduled, container: container) }
+
+ it_behaves_like 'get single container repository storage move' do
+ let(:container_id) { container.id }
+ let(:url) { "/projects/#{container_id}/repository_storage_moves/#{repository_storage_move_id}" }
+ end
+
+ it_behaves_like 'post single container repository storage move'
+ end
end
end
diff --git a/spec/requests/api/project_templates_spec.rb b/spec/requests/api/project_templates_spec.rb
index e1d156afd54..1987d70633b 100644
--- a/spec/requests/api/project_templates_spec.rb
+++ b/spec/requests/api/project_templates_spec.rb
@@ -69,7 +69,7 @@ RSpec.describe API::ProjectTemplates, feature_category: :source_code_management
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
expect(response).to match_response_schema('public_api/v4/template_list')
- expect(json_response.map { |t| t['key'] }).to match_array(%w(bug feature_proposal template_test))
+ expect(json_response.map { |t| t['key'] }).to match_array(%w[bug feature_proposal template_test])
end
it 'returns merge request templates' do
@@ -78,7 +78,7 @@ RSpec.describe API::ProjectTemplates, feature_category: :source_code_management
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
expect(response).to match_response_schema('public_api/v4/template_list')
- expect(json_response.map { |t| t['key'] }).to match_array(%w(bug feature_proposal template_test))
+ expect(json_response.map { |t| t['key'] }).to match_array(%w[bug feature_proposal template_test])
end
it 'returns 400 for an unknown template type' do
diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb
index 64e010aa50f..e9319d514aa 100644
--- a/spec/requests/api/projects_spec.rb
+++ b/spec/requests/api/projects_spec.rb
@@ -286,6 +286,32 @@ RSpec.describe API::Projects, :aggregate_failures, feature_category: :groups_and
expect(json_response.map { |p| p['id'] }).not_to include(project.id)
end
+ context 'when user requests pending_delete projects' do
+ before do
+ project.update!(pending_delete: true)
+ end
+
+ let(:params) { { include_pending_delete: true } }
+
+ it 'does not return projects marked for deletion' do
+ get api(path, user), params: params
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response).to be_an Array
+ expect(json_response.map { |p| p['id'] }).not_to include(project.id)
+ end
+
+ context 'when user is an admin' do
+ it 'returns projects marked for deletion' do
+ get api(path, admin, admin_mode: true), params: params
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response).to be_an Array
+ expect(json_response.map { |p| p['id'] }).to include(project.id)
+ end
+ end
+ end
+
it 'does not include open_issues_count if issues are disabled' do
project.project_feature.update_attribute(:issues_access_level, ProjectFeature::DISABLED)
@@ -299,7 +325,7 @@ RSpec.describe API::Projects, :aggregate_failures, feature_category: :groups_and
context 'filter by topic (column topic_list)' do
before do
- project.update!(topic_list: %w(ruby javascript))
+ project.update!(topic_list: %w[ruby javascript])
end
it 'returns no projects' do
@@ -868,7 +894,7 @@ RSpec.describe API::Projects, :aggregate_failures, feature_category: :groups_and
context 'sorting' do
context 'by project statistics' do
- %w(repository_size storage_size wiki_size packages_size).each do |order_by|
+ %w[repository_size storage_size wiki_size packages_size].each do |order_by|
context "sorting by #{order_by}" do
before do
ProjectStatistics.update_all(order_by => 100)
@@ -1400,13 +1426,14 @@ RSpec.describe API::Projects, :aggregate_failures, feature_category: :groups_and
it 'disallows creating a project with an import_url that is not reachable' do
url = 'http://example.com'
endpoint_url = "#{url}/info/refs?service=git-upload-pack"
- stub_full_request(endpoint_url, method: :get).to_return({ status: 301, body: '', headers: nil })
+ error_response = { status: 301, body: '', headers: nil }
+ stub_full_request(endpoint_url, method: :get).to_return(error_response)
project_params = { import_url: url, path: 'path-project-Foo', name: 'Foo Project' }
expect { post api(path, user), params: project_params }.not_to change { Project.count }
expect(response).to have_gitlab_http_status(:unprocessable_entity)
- expect(json_response['message']).to eq("#{url} is not a valid HTTP Git repository")
+ expect(json_response['message']).to eq("#{url} endpoint error: #{error_response[:status]}")
end
it 'creates a project with an import_url that is valid' do
@@ -2533,7 +2560,7 @@ RSpec.describe API::Projects, :aggregate_failures, feature_category: :groups_and
context 'and the project has a private repository' do
let(:project) { create(:project, :repository, :public, :repository_private) }
- let(:protected_attributes) { %w(default_branch ci_config_path) }
+ let(:protected_attributes) { %w[default_branch ci_config_path] }
it 'hides protected attributes of private repositories if user is not a member' do
get api(path, user)
@@ -2782,10 +2809,20 @@ RSpec.describe API::Projects, :aggregate_failures, feature_category: :groups_and
expect(json_response['shared_with_groups'][0]['expires_at']).to eq(expires_at.to_s)
end
- it 'returns a project by path name' do
- get api(path, user)
- expect(response).to have_gitlab_http_status(:ok)
- expect(json_response['name']).to eq(project.name)
+ context 'when path name is specified' do
+ it 'returns a project' do
+ get api("/projects/#{CGI.escape(project.full_path)}", user)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response['name']).to eq(project.name)
+ end
+
+ it 'returns a project using case-insensitive search' do
+ get api("/projects/#{CGI.escape(project.full_path.swapcase)}", user)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response['name']).to eq(project.name)
+ end
end
context 'when a project is moved' do
@@ -3688,6 +3725,16 @@ RSpec.describe API::Projects, :aggregate_failures, feature_category: :groups_and
it_behaves_like '412 response' do
subject(:request) { api("/projects/#{project.id}/share/#{group.id}", user) }
end
+
+ it "returns an error when link is not destroyed" do
+ allow(::Projects::GroupLinks::DestroyService).to receive_message_chain(:new, :execute)
+ .and_return(ServiceResponse.error(message: '404 Not Found', reason: :not_found))
+
+ delete api("/projects/#{project.id}/share/#{group.id}", user)
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ expect(json_response['message']).to eq '404 Not Found'
+ end
end
it 'returns a 400 when group id is not an integer' do
@@ -3893,7 +3940,7 @@ RSpec.describe API::Projects, :aggregate_failures, feature_category: :groups_and
expect(Project.find_by(path: project[:path]).analytics_access_level).to eq(ProjectFeature::PRIVATE)
end
- %i(releases_access_level environments_access_level feature_flags_access_level infrastructure_access_level monitor_access_level model_experiments_access_level).each do |field|
+ %i[releases_access_level environments_access_level feature_flags_access_level infrastructure_access_level monitor_access_level model_experiments_access_level].each do |field|
it "sets #{field}" do
put api(path, user), params: { field => 'private' }
diff --git a/spec/requests/api/pypi_packages_spec.rb b/spec/requests/api/pypi_packages_spec.rb
index 0b2641b062c..9305155d285 100644
--- a/spec/requests/api/pypi_packages_spec.rb
+++ b/spec/requests/api/pypi_packages_spec.rb
@@ -207,7 +207,22 @@ RSpec.describe API::PypiPackages, feature_category: :package_registry do
let(:url) { "/projects/#{project.id}/packages/pypi" }
let(:headers) { {} }
let(:requires_python) { '>=3.7' }
- let(:base_params) { { requires_python: requires_python, version: '1.0.0', name: 'sample-project', sha256_digest: '1' * 64, md5_digest: '1' * 32 } }
+ let(:base_params) do
+ {
+ requires_python: requires_python,
+ version: '1.0.0',
+ name: 'sample-project',
+ sha256_digest: '1' * 64,
+ md5_digest: '1' * 32,
+ metadata_version: '2.3',
+ author_email: 'cschultz@example.com, snoopy@peanuts.com',
+ description: 'Example description',
+ description_content_type: 'text/plain',
+ summary: 'A module for collecting votes from beagles.',
+ keywords: 'dog,puppy,voting,election'
+ }
+ end
+
let(:params) { base_params.merge(content: temp_file(file_name)) }
let(:send_rewritten_field) { true }
let(:snowplow_gitlab_standard_context) { { project: project, namespace: project.namespace, user: user, property: 'i_package_pypi_user' } }
diff --git a/spec/requests/api/releases_spec.rb b/spec/requests/api/releases_spec.rb
index a018b91019b..493dc4e72c6 100644
--- a/spec/requests/api/releases_spec.rb
+++ b/spec/requests/api/releases_spec.rb
@@ -1492,7 +1492,7 @@ RSpec.describe API::Releases, :aggregate_failures, feature_category: :release_or
subject
expect(response).to have_gitlab_http_status(:ok)
- expect(returned_milestones).to match_array(%w(milestone2 milestone3))
+ expect(returned_milestones).to match_array(%w[milestone2 milestone3])
end
end
diff --git a/spec/requests/api/repositories_spec.rb b/spec/requests/api/repositories_spec.rb
index 22239f1d23f..f38a120cc74 100644
--- a/spec/requests/api/repositories_spec.rb
+++ b/spec/requests/api/repositories_spec.rb
@@ -742,7 +742,7 @@ RSpec.describe API::Repositories, feature_category: :source_code_management do
describe 'GET :id/repository/merge_base' do
let(:refs) do
- %w(304d257dcb821665ab5110318fc58a007bd104ed 0031876facac3f2b2702a0e53a26e89939a42209 570e7b2abdd848b95f2f578043fc23bd6f6fd24d)
+ %w[304d257dcb821665ab5110318fc58a007bd104ed 0031876facac3f2b2702a0e53a26e89939a42209 570e7b2abdd848b95f2f578043fc23bd6f6fd24d]
end
subject(:request) do
@@ -786,7 +786,7 @@ RSpec.describe API::Repositories, feature_category: :source_code_management do
context 'when passing refs that do not exist' do
it_behaves_like '400 response' do
- let(:refs) { %w(304d257dcb821665ab5110318fc58a007bd104ed missing) }
+ let(:refs) { %w[304d257dcb821665ab5110318fc58a007bd104ed missing] }
let(:current_user) { user }
let(:message) { 'Could not find ref: missing' }
end
@@ -801,7 +801,7 @@ RSpec.describe API::Repositories, feature_category: :source_code_management do
end
context 'when not enough refs are passed' do
- let(:refs) { %w(only-one) }
+ let(:refs) { %w[only-one] }
let(:current_user) { user }
it 'renders a bad request error' do
diff --git a/spec/requests/api/resource_access_tokens_spec.rb b/spec/requests/api/resource_access_tokens_spec.rb
index dcb6572d413..01e02651a64 100644
--- a/spec/requests/api/resource_access_tokens_spec.rb
+++ b/spec/requests/api/resource_access_tokens_spec.rb
@@ -477,6 +477,7 @@ RSpec.describe API::ResourceAccessTokens, feature_category: :system_access do
let_it_be(:token) { create(:personal_access_token, user: project_bot) }
let_it_be(:resource_id) { resource.id }
let_it_be(:token_id) { token.id }
+ let(:params) { {} }
let(:path) { "/#{source_type}s/#{resource_id}/access_tokens/#{token_id}/rotate" }
@@ -485,7 +486,7 @@ RSpec.describe API::ResourceAccessTokens, feature_category: :system_access do
resource.add_owner(user)
end
- subject(:rotate_token) { post api(path, user) }
+ subject(:rotate_token) { post(api(path, user), params: params) }
it "allows owner to rotate token", :freeze_time do
rotate_token
@@ -495,6 +496,19 @@ RSpec.describe API::ResourceAccessTokens, feature_category: :system_access do
expect(json_response['expires_at']).to eq((Date.today + 1.week).to_s)
end
+ context 'when expiry is defined' do
+ let(:expiry_date) { Date.today + 1.month }
+ let(:params) { { expires_at: expiry_date } }
+
+ it "allows owner to rotate token", :freeze_time do
+ rotate_token
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response['token']).not_to eq(token.token)
+ expect(json_response['expires_at']).to eq(expiry_date.to_s)
+ end
+ end
+
context 'without permission' do
it 'returns an error message' do
another_user = create(:user)
diff --git a/spec/requests/api/search_spec.rb b/spec/requests/api/search_spec.rb
index 6a57cf52466..4733fdafbfb 100644
--- a/spec/requests/api/search_spec.rb
+++ b/spec/requests/api/search_spec.rb
@@ -140,14 +140,14 @@ RSpec.describe API::Search, :clean_gitlab_redis_rate_limiting, feature_category:
end
context 'when DB timeouts occur from global searches', :aggregate_failures do
- %w(
+ %w[
issues
merge_requests
milestones
projects
snippet_titles
users
- ).each do |scope|
+ ].each do |scope|
it "returns a 408 error if search with scope: #{scope} times out" do
allow(SearchService).to receive(:new).and_raise ActiveRecord::QueryCanceled
get api(endpoint, user), params: { scope: scope, search: 'awesome' }
diff --git a/spec/requests/api/settings_spec.rb b/spec/requests/api/settings_spec.rb
index 2fdcf710471..5656fda7684 100644
--- a/spec/requests/api/settings_spec.rb
+++ b/spec/requests/api/settings_spec.rb
@@ -79,7 +79,7 @@ RSpec.describe API::Settings, 'Settings', :do_not_mock_admin_mode_setting, featu
expect(json_response['slack_app_secret']).to be_nil
expect(json_response['slack_app_signing_secret']).to be_nil
expect(json_response['slack_app_verification_token']).to be_nil
- expect(json_response['valid_runner_registrars']).to match_array(%w(project group))
+ expect(json_response['valid_runner_registrars']).to match_array(%w[project group])
expect(json_response['ci_max_includes']).to eq(150)
expect(json_response['allow_account_deletion']).to eq(true)
expect(json_response['gitlab_shell_operation_limit']).to eq(600)
@@ -261,7 +261,7 @@ RSpec.describe API::Settings, 'Settings', :do_not_mock_admin_mode_setting, featu
expect(json_response['max_decompressed_archive_size']).to eq(20000)
expect(json_response['max_terraform_state_size_bytes']).to eq(1_000)
expect(json_response['disabled_oauth_sign_in_sources']).to eq([])
- expect(json_response['import_sources']).to match_array(%w(github bitbucket))
+ expect(json_response['import_sources']).to match_array(%w[github bitbucket])
expect(json_response['wiki_page_max_content_bytes']).to eq(12345)
expect(json_response['wiki_asciidoc_allow_uri_includes']).to be(true)
expect(json_response['personal_access_token_prefix']).to eq("GL-")
@@ -418,12 +418,12 @@ RSpec.describe API::Settings, 'Settings', :do_not_mock_admin_mode_setting, featu
end
it 'does not allow unrestricted key lengths' do
- types = %w(dsa_key_restriction
+ types = %w[dsa_key_restriction
ecdsa_key_restriction
ecdsa_sk_key_restriction
ed25519_key_restriction
ed25519_sk_key_restriction
- rsa_key_restriction)
+ rsa_key_restriction]
types.each do |type|
put api("/application/settings", admin), params: { type => 0 }
@@ -519,7 +519,7 @@ RSpec.describe API::Settings, 'Settings', :do_not_mock_admin_mode_setting, featu
context 'EKS integration settings' do
let(:attribute_names) { settings.keys.map(&:to_s) }
- let(:sensitive_attributes) { %w(eks_secret_access_key) }
+ let(:sensitive_attributes) { %w[eks_secret_access_key] }
let(:exposed_attributes) { attribute_names - sensitive_attributes }
let(:settings) do
diff --git a/spec/requests/api/tags_spec.rb b/spec/requests/api/tags_spec.rb
index 604631bbf7f..6bbd43bfc14 100644
--- a/spec/requests/api/tags_spec.rb
+++ b/spec/requests/api/tags_spec.rb
@@ -206,7 +206,7 @@ RSpec.describe API::Tags, feature_category: :source_code_management do
expect(response).to match_response_schema('public_api/v4/tags')
expect(response.headers).to include('Link')
tag_names = json_response.map { |x| x['name'] }
- expect(tag_names).to match_array(%w(v1.1.0 v1.1.1))
+ expect(tag_names).to match_array(%w[v1.1.0 v1.1.1])
end
end
diff --git a/spec/requests/api/task_completion_status_spec.rb b/spec/requests/api/task_completion_status_spec.rb
index c46d6954da3..ae48b0c18cd 100644
--- a/spec/requests/api/task_completion_status_spec.rb
+++ b/spec/requests/api/task_completion_status_spec.rb
@@ -21,30 +21,30 @@ RSpec.describe 'task completion status response', features: :team_planning do
expected_completed_count: 0
},
{
- description: %{- [ ] task 1
- - [x] task 2 },
+ description: %(- [ ] task 1
+ - [x] task 2 ),
expected_count: 2,
expected_completed_count: 1
},
{
- description: %{- [ ] task 1
- - [ ] task 2 },
+ description: %(- [ ] task 1
+ - [ ] task 2 ),
expected_count: 2,
expected_completed_count: 0
},
{
- description: %{- [x] task 1
- - [x] task 2 },
+ description: %(- [x] task 1
+ - [x] task 2 ),
expected_count: 2,
expected_completed_count: 2
},
{
- description: %{- [ ] task 1},
+ description: %(- [ ] task 1),
expected_count: 1,
expected_completed_count: 0
},
{
- description: %{- [x] task 1},
+ description: %(- [x] task 1),
expected_count: 1,
expected_completed_count: 1
}
diff --git a/spec/requests/api/unleash_spec.rb b/spec/requests/api/unleash_spec.rb
index 75b26b98228..050be3ae8aa 100644
--- a/spec/requests/api/unleash_spec.rb
+++ b/spec/requests/api/unleash_spec.rb
@@ -96,7 +96,7 @@ RSpec.describe API::Unleash, feature_category: :feature_flags do
end
end
- %w(/feature_flags/unleash/:project_id/features /feature_flags/unleash/:project_id/client/features).each do |features_endpoint|
+ %w[/feature_flags/unleash/:project_id/features /feature_flags/unleash/:project_id/client/features].each do |features_endpoint|
describe "GET #{features_endpoint}", :use_clean_rails_redis_caching do
let(:features_url) { features_endpoint.sub(':project_id', project_id.to_s) }
let(:client) { create(:operations_feature_flags_client, project: project) }
diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb
index 7da44266064..76fe72efc64 100644
--- a/spec/requests/api/users_spec.rb
+++ b/spec/requests/api/users_spec.rb
@@ -5,6 +5,7 @@ require 'spec_helper'
RSpec.describe API::Users, :aggregate_failures, feature_category: :user_profile do
include WorkhorseHelpers
include KeysetPaginationHelpers
+ include CryptoHelpers
let_it_be(:admin) { create(:admin) }
let_it_be(:user, reload: true) { create(:user, username: 'user.withdot') }
@@ -226,6 +227,19 @@ RSpec.describe API::Users, :aggregate_failures, feature_category: :user_profile
end
end
+ context 'with search parameter' do
+ let_it_be(:first_user) { create(:user, username: 'a-user') }
+ let_it_be(:second_user) { create(:user, username: 'a-user2') }
+
+ it 'prioritizes username match' do
+ get api(path, user, admin_mode: true), params: { search: first_user.username }
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response.first['username']).to eq('a-user')
+ expect(json_response.second['username']).to eq('a-user2')
+ end
+ end
+
context 'N+1 queries' do
before do
create_list(:user, 2)
@@ -1983,17 +1997,23 @@ RSpec.describe API::Users, :aggregate_failures, feature_category: :user_profile
end
describe "PUT /user/:id/credit_card_validation" do
- let(:credit_card_validated_time) { Time.utc(2020, 1, 1) }
+ let(:network) { 'American Express' }
+ let(:holder_name) { 'John Smith' }
+ let(:last_digits) { '1111' }
let(:expiration_year) { Date.today.year + 10 }
+ let(:expiration_month) { 1 }
+ let(:expiration_date) { Date.new(expiration_year, expiration_month, -1) }
+ let(:credit_card_validated_at) { Time.utc(2020, 1, 1) }
+
let(:path) { "/user/#{user.id}/credit_card_validation" }
let(:params) do
{
- credit_card_validated_at: credit_card_validated_time,
+ credit_card_validated_at: credit_card_validated_at,
credit_card_expiration_year: expiration_year,
- credit_card_expiration_month: 1,
- credit_card_holder_name: 'John Smith',
- credit_card_type: 'AmericanExpress',
- credit_card_mask_number: '1111'
+ credit_card_expiration_month: expiration_month,
+ credit_card_holder_name: holder_name,
+ credit_card_type: network,
+ credit_card_mask_number: last_digits
}
end
@@ -2023,11 +2043,11 @@ RSpec.describe API::Users, :aggregate_failures, feature_category: :user_profile
expect(response).to have_gitlab_http_status(:ok)
expect(user.credit_card_validation).to have_attributes(
- credit_card_validated_at: credit_card_validated_time,
- expiration_date: Date.new(expiration_year, 1, 31),
- last_digits: 1111,
- network: 'AmericanExpress',
- holder_name: 'John Smith'
+ credit_card_validated_at: credit_card_validated_at,
+ network_hash: sha256(network.downcase),
+ holder_name_hash: sha256(holder_name.downcase),
+ last_digits_hash: sha256(last_digits),
+ expiration_date_hash: sha256(expiration_date.to_s)
)
end
@@ -4519,7 +4539,7 @@ RSpec.describe API::Users, :aggregate_failures, feature_category: :user_profile
describe 'POST /users/:user_id/personal_access_tokens' do
let(:name) { 'new pat' }
let(:expires_at) { 3.days.from_now.to_date.to_s }
- let(:scopes) { %w(api read_user) }
+ let(:scopes) { %w[api read_user] }
let(:path) { "/users/#{user.id}/personal_access_tokens" }
let(:params) { { name: name, scopes: scopes, expires_at: expires_at } }
diff --git a/spec/requests/api/v3/github_spec.rb b/spec/requests/api/v3/github_spec.rb
deleted file mode 100644
index fbda291e901..00000000000
--- a/spec/requests/api/v3/github_spec.rb
+++ /dev/null
@@ -1,721 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe API::V3::Github, :aggregate_failures, feature_category: :integrations do
- let_it_be(:user) { create(:user) }
- let_it_be(:unauthorized_user) { create(:user) }
- let_it_be(:admin) { create(:user, :admin) }
- let_it_be_with_reload(:project) { create(:project, :repository, creator: user) }
-
- before do
- project.add_maintainer(user) if user
- end
-
- describe 'GET /orgs/:namespace/repos' do
- let_it_be(:group) { create(:group) }
-
- it_behaves_like 'a GitHub Enterprise Jira DVCS reversible end of life endpoint' do
- subject do
- jira_get v3_api("/orgs/#{group.path}/repos", user)
- end
- end
-
- it 'logs when the endpoint is hit and `jira_dvcs_end_of_life_amnesty` is enabled' do
- expect(Gitlab::JsonLogger).to receive(:info).with(
- including(
- namespace: group.path,
- user_id: user.id,
- message: 'Deprecated Jira DVCS endpoint request'
- )
- )
-
- jira_get v3_api("/orgs/#{group.path}/repos", user)
-
- stub_feature_flags(jira_dvcs_end_of_life_amnesty: false)
-
- expect(Gitlab::JsonLogger).not_to receive(:info)
-
- jira_get v3_api("/orgs/#{group.path}/repos", user)
- end
-
- it 'returns an empty array' do
- jira_get v3_api("/orgs/#{group.path}/repos", user)
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(json_response).to eq([])
- end
-
- it 'returns 200 when namespace path include a dot' do
- group = create(:group, path: 'foo.bar')
-
- jira_get v3_api("/orgs/#{group.path}/repos", user)
-
- expect(response).to have_gitlab_http_status(:ok)
- end
- end
-
- describe 'GET /user/repos' do
- it_behaves_like 'a GitHub Enterprise Jira DVCS reversible end of life endpoint' do
- subject { jira_get v3_api('/user/repos', user) }
- end
-
- it 'returns an empty array' do
- jira_get v3_api('/user/repos', user)
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(json_response).to eq([])
- end
- end
-
- shared_examples_for 'Jira-specific mimicked GitHub endpoints' do
- describe 'GET /.../issues/:id/comments' do
- let(:merge_request) do
- create(:merge_request, source_project: project, target_project: project)
- end
-
- let!(:note) do
- create(:note, project: project, noteable: merge_request)
- end
-
- context 'when user has access to the merge request' do
- it 'returns an array of notes' do
- jira_get v3_api("/repos/#{path}/issues/#{merge_request.id}/comments", user)
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(json_response).to be_an(Array)
- expect(json_response.size).to eq(1)
- end
- end
-
- context 'when user has no access to the merge request' do
- let(:project) { create(:project, :private) }
-
- before do
- project.add_guest(user)
- end
-
- it 'returns 404' do
- jira_get v3_api("/repos/#{path}/issues/#{merge_request.id}/comments", user)
-
- expect(response).to have_gitlab_http_status(:not_found)
- end
- end
- end
-
- describe 'GET /.../pulls/:id/commits' do
- it 'returns an empty array' do
- jira_get v3_api("/repos/#{path}/pulls/xpto/commits", user)
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(json_response).to eq([])
- end
- end
-
- describe 'GET /.../pulls/:id/comments' do
- it 'returns an empty array' do
- jira_get v3_api("/repos/#{path}/pulls/xpto/comments", user)
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(json_response).to eq([])
- end
- end
- end
-
- # Here we test that using /-/jira as namespace/project still works,
- # since that is how old Jira setups will talk to us
- context 'old /-/jira endpoints' do
- it_behaves_like 'Jira-specific mimicked GitHub endpoints' do
- let(:path) { '-/jira' }
- end
-
- it 'returns an empty Array for events' do
- jira_get v3_api('/repos/-/jira/events', user)
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(json_response).to eq([])
- end
- end
-
- context 'new :namespace/:project jira endpoints' do
- it_behaves_like 'Jira-specific mimicked GitHub endpoints' do
- let(:path) { "#{project.namespace.path}/#{project.path}" }
- end
-
- describe 'GET /users/:username' do
- let!(:user1) { create(:user, username: 'jane.porter') }
-
- it_behaves_like 'a GitHub Enterprise Jira DVCS reversible end of life endpoint' do
- subject { jira_get v3_api("/users/#{user.username}", user) }
- end
-
- context 'user exists' do
- it 'responds with the expected user' do
- jira_get v3_api("/users/#{user.username}", user)
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(response).to match_response_schema('entities/github/user')
- end
- end
-
- context 'user does not exist' do
- it 'responds with the expected status' do
- jira_get v3_api('/users/unknown_user_name', user)
-
- expect(response).to have_gitlab_http_status(:not_found)
- end
- end
-
- context 'no rights to request user lists' do
- before do
- expect(Ability).to receive(:allowed?).with(unauthorized_user, :read_users_list, :global).and_return(false)
- expect(Ability).to receive(:allowed?).at_least(:once).and_call_original
- end
-
- it 'responds with forbidden' do
- jira_get v3_api("/users/#{user.username}", unauthorized_user)
-
- expect(response).to have_gitlab_http_status(:forbidden)
- end
- end
- end
-
- describe 'GET events' do
- include ProjectForksHelper
-
- let(:group) { create(:group) }
- let(:project) { create(:project, :empty_repo, path: 'project.with.dot', group: group) }
- let(:events_path) { "/repos/#{group.path}/#{project.path}/events" }
-
- it_behaves_like 'a GitHub Enterprise Jira DVCS reversible end of life endpoint' do
- subject { jira_get v3_api(events_path, user) }
- end
-
- context 'if there are no merge requests' do
- it 'returns an empty array' do
- jira_get v3_api(events_path, user)
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(json_response).to eq([])
- end
- end
-
- context 'if there is a merge request' do
- let!(:merge_request) { create(:merge_request, source_project: project, target_project: project, author: user) }
-
- it 'returns an event' do
- jira_get v3_api(events_path, user)
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(json_response).to be_an(Array)
- expect(json_response.size).to eq(1)
- end
- end
-
- it 'avoids N+1 queries' do
- create(:merge_request, source_project: project)
- source_project = fork_project(project, nil, repository: true)
-
- control_count = ActiveRecord::QueryRecorder.new(skip_cached: false) { jira_get v3_api(events_path, user) }.count
-
- create_list(:merge_request, 2, :unique_branches, source_project: source_project, target_project: project)
-
- expect { jira_get v3_api(events_path, user) }.not_to exceed_all_query_limit(control_count)
- end
-
- context 'if there are more merge requests' do
- let!(:merge_request) { create(:merge_request, id: 10000, source_project: project, target_project: project, author: user) }
- let!(:merge_request2) { create(:merge_request, id: 10001, source_project: project, source_branch: generate(:branch), target_project: project, author: user) }
-
- it 'returns the expected amount of events' do
- jira_get v3_api(events_path, user)
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(json_response).to be_an(Array)
- expect(json_response.size).to eq(2)
- end
-
- it 'ensures each event has a unique id' do
- jira_get v3_api(events_path, user)
-
- ids = json_response.map { |event| event['id'] }.uniq
- expect(ids.size).to eq(2)
- end
- end
- end
- end
-
- describe 'repo pulls' do
- let_it_be(:project2) { create(:project, :repository, creator: user) }
- let_it_be(:assignee) { create(:user) }
- let_it_be(:assignee2) { create(:user) }
- let_it_be(:merge_request) do
- create(:merge_request, source_project: project, target_project: project, author: user, assignees: [assignee])
- end
-
- let_it_be(:merge_request_2) do
- create(:merge_request, source_project: project2, target_project: project2, author: user, assignees: [assignee, assignee2])
- end
-
- before do
- project2.add_maintainer(user)
- end
-
- def perform_request
- jira_get v3_api(route, user)
- end
-
- describe 'GET /-/jira/pulls' do
- let(:route) { '/repos/-/jira/pulls' }
-
- it_behaves_like 'a GitHub Enterprise Jira DVCS reversible end of life endpoint' do
- subject { perform_request }
- end
-
- it 'returns an array of merge requests with github format' do
- perform_request
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(json_response).to be_an(Array)
- expect(json_response.size).to eq(2)
- expect(response).to match_response_schema('entities/github/pull_requests')
- end
-
- it 'returns multiple merge requests without N + 1' do
- perform_request
-
- control_count = ActiveRecord::QueryRecorder.new { perform_request }.count
-
- project3 = create(:project, :repository, creator: user)
- project3.add_maintainer(user)
- assignee3 = create(:user)
- create(:merge_request, source_project: project3, target_project: project3, author: user, assignees: [assignee3])
-
- expect { perform_request }.not_to exceed_query_limit(control_count)
- end
- end
-
- describe 'GET /repos/:namespace/:project/pulls' do
- let(:route) { "/repos/#{project.namespace.path}/#{project.path}/pulls" }
-
- it_behaves_like 'a GitHub Enterprise Jira DVCS reversible end of life endpoint' do
- subject { perform_request }
- end
-
- it 'returns an array of merge requests for the proper project in github format' do
- perform_request
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(json_response).to be_an(Array)
- expect(json_response.size).to eq(1)
- expect(response).to match_response_schema('entities/github/pull_requests')
- end
-
- it 'returns multiple merge requests without N + 1' do
- perform_request
-
- control_count = ActiveRecord::QueryRecorder.new { perform_request }.count
-
- create(:merge_request, source_project: project, source_branch: 'fix')
-
- expect { perform_request }.not_to exceed_query_limit(control_count)
- end
- end
-
- describe 'GET /repos/:namespace/:project/pulls/:id' do
- it_behaves_like 'a GitHub Enterprise Jira DVCS reversible end of life endpoint' do
- subject { jira_get v3_api("/repos/#{project.namespace.path}/#{project.path}/pulls/#{merge_request.id}", user) }
- end
-
- context 'when user has access to the merge requests' do
- it 'returns the requested merge request in github format' do
- jira_get v3_api("/repos/#{project.namespace.path}/#{project.path}/pulls/#{merge_request.id}", user)
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(response).to match_response_schema('entities/github/pull_request')
- end
- end
-
- context 'when user has no access to the merge request' do
- it 'returns 404' do
- project.add_guest(unauthorized_user)
-
- jira_get v3_api("/repos/#{project.namespace.path}/#{project.path}/pulls/#{merge_request.id}", unauthorized_user)
-
- expect(response).to have_gitlab_http_status(:not_found)
- end
- end
-
- context 'when instance admin' do
- it 'returns the requested merge request in github format' do
- jira_get v3_api("/repos/#{project.namespace.path}/#{project.path}/pulls/#{merge_request.id}", admin, admin_mode: true)
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(response).to match_response_schema('entities/github/pull_request')
- end
- end
- end
- end
-
- describe 'GET /users/:namespace/repos' do
- let(:group) { create(:group, name: 'foo') }
-
- def expect_project_under_namespace(projects, namespace, user, admin_mode = false)
- jira_get v3_api("/users/#{namespace.path}/repos", user, admin_mode: admin_mode)
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(response).to include_pagination_headers
- expect(response).to match_response_schema('entities/github/repositories')
-
- projects.each do |project|
- hash = json_response.find do |hash|
- hash['name'] == ::Gitlab::Jira::Dvcs.encode_project_name(project)
- end
-
- raise "Project #{project.full_path} not present in response" if hash.nil?
-
- expect(hash['owner']['login']).to eq(namespace.path)
- end
- expect(json_response.size).to eq(projects.size)
- end
-
- it_behaves_like 'a GitHub Enterprise Jira DVCS reversible end of life endpoint' do
- subject { jira_get v3_api("/users/#{user.namespace.path}/repos", user) }
- end
-
- context 'group namespace' do
- let(:project) { create(:project, group: group) }
- let!(:project2) { create(:project, :public, group: group) }
-
- it 'returns an array of projects belonging to group excluding the ones user is not directly a member of, even when public' do
- expect_project_under_namespace([project], group, user)
- end
-
- context 'when instance admin' do
- let(:user) { create(:user, :admin) }
-
- it 'returns an array of projects belonging to group' do
- expect_project_under_namespace([project, project2], group, user, true)
- end
-
- context 'with a private group' do
- let(:group) { create(:group, :private) }
- let!(:project2) { create(:project, :private, group: group) }
-
- it 'returns an array of projects belonging to group' do
- expect_project_under_namespace([project, project2], group, user, true)
- end
- end
- end
- end
-
- context 'nested group namespace' do
- let(:group) { create(:group, :nested) }
- let!(:parent_group_project) { create(:project, group: group.parent, name: 'parent_group_project') }
- let!(:child_group_project) { create(:project, group: group, name: 'child_group_project') }
-
- before do
- group.parent.add_maintainer(user)
- end
-
- it 'returns an array of projects belonging to group with github format' do
- expect_project_under_namespace([parent_group_project, child_group_project], group.parent, user)
- end
-
- it 'avoids N+1 queries' do
- jira_get v3_api("/users/#{group.parent.path}/repos", user)
-
- control = ActiveRecord::QueryRecorder.new { jira_get v3_api("/users/#{group.parent.path}/repos", user) }
-
- new_group = create(:group, parent: group.parent)
- create(:project, :repository, group: new_group, creator: user)
-
- expect { jira_get v3_api("/users/#{group.parent.path}/repos", user) }.not_to exceed_query_limit(control)
- expect(response).to have_gitlab_http_status(:ok)
- end
- end
-
- context 'user namespace' do
- let(:project) { create(:project, namespace: user.namespace) }
-
- it 'returns an array of projects belonging to user namespace with github format' do
- expect_project_under_namespace([project], user.namespace, user)
- end
- end
-
- context 'namespace path includes a dot' do
- let(:project) { create(:project, group: group) }
- let(:group) { create(:group, name: 'foo.bar') }
-
- before do
- group.add_maintainer(user)
- end
-
- it 'returns an array of projects belonging to group with github format' do
- expect_project_under_namespace([project], group, user)
- end
- end
-
- context 'unauthenticated' do
- it 'returns 401' do
- jira_get v3_api('/users/foo/repos', nil)
-
- expect(response).to have_gitlab_http_status(:unauthorized)
- end
- end
-
- context 'namespace does not exist' do
- it 'responds with not found status' do
- jira_get v3_api('/users/noo/repos', user)
-
- expect(response).to have_gitlab_http_status(:not_found)
- end
- end
- end
-
- describe 'GET /repos/:namespace/:project/branches' do
- context 'authenticated' do
- it_behaves_like 'a GitHub Enterprise Jira DVCS reversible end of life endpoint' do
- subject { jira_get v3_api("/repos/#{project.namespace.path}/#{project.path}/branches", user) }
- end
-
- context 'updating project feature usage' do
- it 'counts Jira Cloud integration as enabled' do
- user_agent = 'Jira DVCS Connector Vertigo/4.42.0'
-
- freeze_time do
- jira_get v3_api("/repos/#{project.namespace.path}/#{project.path}/branches", user), user_agent
-
- expect(project.reload.jira_dvcs_cloud_last_sync_at).to be_like_time(Time.now)
- end
- end
-
- it 'counts Jira Server integration as enabled' do
- user_agent = 'Jira DVCS Connector/3.2.4'
-
- freeze_time do
- jira_get v3_api("/repos/#{project.namespace.path}/#{project.path}/branches", user), user_agent
-
- expect(project.reload.jira_dvcs_server_last_sync_at).to be_like_time(Time.now)
- end
- end
- end
-
- it 'returns an array of project branches with github format' do
- jira_get v3_api("/repos/#{project.namespace.path}/#{project.path}/branches", user)
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(response).to include_pagination_headers
- expect(json_response).to be_an(Array)
-
- expect(response).to match_response_schema('entities/github/branches')
- end
-
- it 'returns 200 when project path include a dot' do
- project.update!(path: 'foo.bar')
-
- jira_get v3_api("/repos/#{project.namespace.path}/#{project.path}/branches", user)
-
- expect(response).to have_gitlab_http_status(:ok)
- end
-
- it 'returns 200 when namespace path include a dot' do
- group = create(:group, path: 'foo.bar')
- project = create(:project, :repository, group: group)
- project.add_reporter(user)
-
- jira_get v3_api("/repos/#{group.path}/#{project.path}/branches", user)
-
- expect(response).to have_gitlab_http_status(:ok)
- end
-
- context 'when the project has no repository' do
- let_it_be(:project) { create(:project, creator: user) }
-
- it 'returns an empty collection response' do
- jira_get v3_api("/repos/#{project.namespace.path}/#{project.path}/branches", user)
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(json_response).to be_empty
- end
- end
- end
-
- context 'unauthenticated' do
- it 'returns 401' do
- jira_get v3_api("/repos/#{project.namespace.path}/#{project.path}/branches", nil)
-
- expect(response).to have_gitlab_http_status(:unauthorized)
- end
- end
-
- context 'unauthorized' do
- it 'returns 404 when lower access level' do
- project.add_guest(unauthorized_user)
-
- jira_get v3_api("/repos/#{project.namespace.path}/#{project.path}/branches", unauthorized_user)
-
- expect(response).to have_gitlab_http_status(:not_found)
- end
- end
- end
-
- describe 'GET /repos/:namespace/:project/commits/:sha' do
- let(:commit) { project.repository.commit }
-
- def call_api(commit_id: commit.id)
- jira_get v3_api("/repos/#{project.namespace.path}/#{project.path}/commits/#{commit_id}", user)
- end
-
- def response_diff_files(response)
- Gitlab::Json.parse(response.body)['files']
- end
-
- context 'authenticated' do
- it_behaves_like 'a GitHub Enterprise Jira DVCS reversible end of life endpoint' do
- subject { call_api }
- end
-
- it 'returns commit with github format' do
- call_api
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(response).to match_response_schema('entities/github/commit')
- end
-
- it 'returns 200 when project path include a dot' do
- project.update!(path: 'foo.bar')
-
- call_api
-
- expect(response).to have_gitlab_http_status(:ok)
- end
-
- context 'when namespace path includes a dot' do
- let(:group) { create(:group, path: 'foo.bar') }
- let(:project) { create(:project, :repository, group: group) }
-
- it 'returns 200 when namespace path include a dot' do
- project.add_reporter(user)
-
- call_api
-
- expect(response).to have_gitlab_http_status(:ok)
- end
- end
-
- context 'when the Gitaly `CommitDiff` RPC times out', :use_clean_rails_memory_store_caching do
- let(:commit_diff_args) { [project.repository_storage, :diff_service, :commit_diff, any_args] }
-
- before do
- allow(Gitlab::GitalyClient).to receive(:call)
- .and_call_original
- end
-
- it 'handles the error, logs it, and returns empty diff files' do
- allow(Gitlab::GitalyClient).to receive(:call)
- .with(*commit_diff_args)
- .and_raise(GRPC::DeadlineExceeded)
-
- expect(Gitlab::ErrorTracking)
- .to receive(:track_exception)
- .with an_instance_of(GRPC::DeadlineExceeded)
-
- call_api
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(response_diff_files(response)).to be_blank
- end
-
- it 'only calls Gitaly once for all attempts within a period of time' do
- expect(Gitlab::GitalyClient).to receive(:call)
- .with(*commit_diff_args)
- .once # <- once
- .and_raise(GRPC::DeadlineExceeded)
-
- 3.times do
- call_api
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(response_diff_files(response)).to be_blank
- end
- end
-
- it 'calls Gitaly again after a period of time' do
- expect(Gitlab::GitalyClient).to receive(:call)
- .with(*commit_diff_args)
- .twice # <- twice
- .and_raise(GRPC::DeadlineExceeded)
-
- call_api
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(response_diff_files(response)).to be_blank
-
- travel_to((described_class::GITALY_TIMEOUT_CACHE_EXPIRY + 1.second).from_now) do
- call_api
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(response_diff_files(response)).to be_blank
- end
- end
-
- it 'uses a unique cache key, allowing other calls to succeed' do
- cache_key = [described_class::GITALY_TIMEOUT_CACHE_KEY, project.id, commit.cache_key].join(':')
- Rails.cache.write(cache_key, 1)
-
- expect(Gitlab::GitalyClient).to receive(:call)
- .with(*commit_diff_args)
- .once # <- once
-
- call_api
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(response_diff_files(response)).to be_blank
-
- call_api(commit_id: commit.parent.id)
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(response_diff_files(response).length).to eq(1)
- end
- end
- end
-
- context 'unauthenticated' do
- let(:user) { nil }
-
- it 'returns 401' do
- call_api
-
- expect(response).to have_gitlab_http_status(:unauthorized)
- end
- end
-
- context 'unauthorized' do
- let(:user) { unauthorized_user }
-
- it 'returns 404 when lower access level' do
- project.add_guest(user)
-
- call_api
-
- expect(response).to have_gitlab_http_status(:not_found)
- end
- end
- end
-
- def jira_get(path, user_agent = 'Jira DVCS Connector/3.2.4')
- get path, headers: { 'User-Agent' => user_agent }
- end
-
- def v3_api(path, user = nil, personal_access_token: nil, oauth_access_token: nil, admin_mode: false)
- api(
- path,
- user,
- version: 'v3',
- personal_access_token: personal_access_token,
- oauth_access_token: oauth_access_token,
- admin_mode: admin_mode
- )
- end
-end
diff --git a/spec/requests/api/vs_code/settings/vs_code_settings_sync_spec.rb b/spec/requests/api/vs_code/settings/vs_code_settings_sync_spec.rb
index 1055a8efded..74d19f8533c 100644
--- a/spec/requests/api/vs_code/settings/vs_code_settings_sync_spec.rb
+++ b/spec/requests/api/vs_code/settings/vs_code_settings_sync_spec.rb
@@ -3,6 +3,8 @@
require 'spec_helper'
RSpec.describe API::VsCode::Settings::VsCodeSettingsSync, :aggregate_failures, factory_default: :keep, feature_category: :web_ide do
+ include GrapePathHelpers::NamedRouteMatcher
+
let_it_be(:user) { create_default(:user) }
let_it_be(:user_token) { create(:personal_access_token) }
@@ -21,6 +23,14 @@ RSpec.describe API::VsCode::Settings::VsCodeSettingsSync, :aggregate_failures, f
end
end
+ shared_examples "returns 400" do
+ it 'returns 400' do
+ get api(path, personal_access_token: user_token)
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ end
+ end
+
describe 'GET /vscode/settings_sync/v1/manifest' do
let(:path) { "/vscode/settings_sync/v1/manifest" }
@@ -80,6 +90,12 @@ RSpec.describe API::VsCode::Settings::VsCodeSettingsSync, :aggregate_failures, f
it_behaves_like "returns 20x when authenticated", :no_content
it_behaves_like "returns unauthorized when not authenticated"
+ context "when resource type is invalid" do
+ let(:path) { "/vscode/settings_sync/v1/resource/foo/1" }
+
+ it_behaves_like "returns 400"
+ end
+
context 'when settings with that type are not present' do
it 'returns 204 no content and no content ETag header' do
get api(path, personal_access_token: user_token)
@@ -102,6 +118,55 @@ RSpec.describe API::VsCode::Settings::VsCodeSettingsSync, :aggregate_failures, f
end
end
+ describe 'GET /vscode/settings_sync/v1/resource/:resource_name/' do
+ let(:path) { "/vscode/settings_sync/v1/resource/settings/" }
+
+ context "when resource type is invalid" do
+ let(:path) { "/vscode/settings_sync/v1/resource/foo" }
+
+ it_behaves_like "returns 400"
+ end
+
+ it_behaves_like "returns unauthorized when not authenticated"
+ it_behaves_like "returns 20x when authenticated", :ok
+
+ context 'when settings with that type are not present' do
+ it "returns empty array response" do
+ get api(path, personal_access_token: user_token)
+
+ expect(json_response.length).to eq(0)
+ end
+ end
+
+ context 'when settings with that type are present' do
+ let_it_be(:settings) { create(:vscode_setting, content: '{ "key": "value" }') }
+
+ it 'returns settings with the correct json content' do
+ get api(path, personal_access_token: user_token)
+
+ setting_type = settings[:setting_type]
+ uuid = settings[:uuid]
+
+ resource_ref = "/api/v4/vscode/settings_sync/v1/resource/#{setting_type}/#{uuid}"
+
+ expect(json_response.length).to eq(1)
+ expect(json_response.first['url']).to eq(resource_ref)
+ expect(json_response.first['created']).to eq(settings.updated_at.to_i)
+ end
+ end
+
+ context 'when setting type is machine' do
+ let(:path) { "/vscode/settings_sync/v1/resource/machines/" }
+
+ it 'created field is nil' do
+ get api(path, personal_access_token: user_token)
+
+ expect(json_response.length).to eq(1)
+ expect(json_response.first['created']).to be_nil
+ end
+ end
+ end
+
describe 'POST /vscode/settings_sync/v1/resource/:resource_name' do
let(:path) { "/vscode/settings_sync/v1/resource/settings" }
@@ -138,4 +203,28 @@ RSpec.describe API::VsCode::Settings::VsCodeSettingsSync, :aggregate_failures, f
expect(response).to have_gitlab_http_status(:bad_request)
end
end
+
+ describe 'DELETE /vscode/settings_sync/v1/collection' do
+ let(:path) { "/vscode/settings_sync/v1/collection" }
+
+ subject(:request) do
+ delete api(path, personal_access_token: user_token)
+ end
+
+ it 'returns unauthorized when not authenticated' do
+ delete api(path)
+ expect(response).to have_gitlab_http_status(:unauthorized)
+ end
+
+ context 'when user has one or more setting resources' do
+ before do
+ create(:vscode_setting, setting_type: 'globalState')
+ create(:vscode_setting, setting_type: 'extensions')
+ end
+
+ it 'deletes all user setting resources' do
+ expect { request }.to change { User.find(user.id).vscode_settings.count }.from(2).to(0)
+ end
+ end
+ end
end
diff --git a/spec/requests/api/wikis_spec.rb b/spec/requests/api/wikis_spec.rb
index 00e38a5bb7e..cd9c5637264 100644
--- a/spec/requests/api/wikis_spec.rb
+++ b/spec/requests/api/wikis_spec.rb
@@ -31,8 +31,8 @@ RSpec.describe API::Wikis, feature_category: :wiki do
let(:project_wiki) { create(:project_wiki, project: project, user: user) }
let(:payload) { { content: 'content', format: 'rdoc', title: 'title' } }
- let(:expected_keys_with_content) { %w(content format slug title encoding) }
- let(:expected_keys_without_content) { %w(format slug title) }
+ let(:expected_keys_with_content) { %w[content format slug title encoding front_matter] }
+ let(:expected_keys_without_content) { %w[format slug title] }
let(:wiki) { project_wiki }
shared_examples_for 'wiki API 404 Project Not Found' do
@@ -354,6 +354,18 @@ RSpec.describe API::Wikis, feature_category: :wiki do
end
include_examples 'wikis API creates wiki page'
+
+ context "with front matter title" do
+ let(:payload) { { title: 'title', front_matter: { "title" => "title in front matter" }, content: 'content' } }
+
+ it "save front matter" do
+ post(api(url, user), params: payload)
+
+ expect(response).to have_gitlab_http_status(:created)
+ expect(json_response['front_matter']).to eq(payload[:front_matter])
+ expect(json_response['content']).to include(payload[:front_matter]["title"])
+ end
+ end
end
context 'when user is maintainer' do
@@ -478,6 +490,20 @@ RSpec.describe API::Wikis, feature_category: :wiki do
include_examples 'wiki API 404 Wiki Page Not Found'
end
+
+ context "with front matter title" do
+ let(:payload) do
+ { title: 'new title', front_matter: { "title" => "title in front matter" }, content: 'new content' }
+ end
+
+ it "save front matter" do
+ put(api(url, user), params: payload)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response['front_matter']).to eq(payload[:front_matter])
+ expect(json_response['content']).to include(payload[:front_matter]["title"])
+ end
+ end
end
context 'when user is maintainer' do
diff --git a/spec/requests/application_controller_spec.rb b/spec/requests/application_controller_spec.rb
deleted file mode 100644
index 52fdf6bc69e..00000000000
--- a/spec/requests/application_controller_spec.rb
+++ /dev/null
@@ -1,15 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe ApplicationController, type: :request, feature_category: :shared do
- let_it_be(:user) { create(:user) }
-
- before do
- sign_in(user)
- end
-
- it_behaves_like 'Base action controller' do
- subject(:request) { get root_path }
- end
-end
diff --git a/spec/requests/chaos_controller_spec.rb b/spec/requests/chaos_controller_spec.rb
deleted file mode 100644
index d2ce618b041..00000000000
--- a/spec/requests/chaos_controller_spec.rb
+++ /dev/null
@@ -1,14 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe ChaosController, type: :request, feature_category: :tooling do
- it_behaves_like 'Base action controller' do
- before do
- # Stub leak_mem so we don't actually leak memory for the base action controller tests.
- allow(Gitlab::Chaos).to receive(:leak_mem).with(100, 30.seconds)
- end
-
- subject(:request) { get leakmem_chaos_path }
- end
-end
diff --git a/spec/requests/explore/catalog_controller_spec.rb b/spec/requests/explore/catalog_controller_spec.rb
new file mode 100644
index 00000000000..50a2240e040
--- /dev/null
+++ b/spec/requests/explore/catalog_controller_spec.rb
@@ -0,0 +1,53 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Explore::CatalogController, feature_category: :pipeline_composition do
+ let_it_be(:user) { create(:user) }
+
+ before do
+ sign_in(user)
+ end
+
+ shared_examples 'basic get requests' do |action|
+ let(:path) do
+ if action == :index
+ explore_catalog_index_path
+ else
+ explore_catalog_path(id: 1)
+ end
+ end
+
+ context 'with FF `global_ci_catalog`' do
+ before do
+ stub_feature_flags(global_ci_catalog: true)
+ end
+
+ it 'responds with 200' do
+ get path
+
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+ end
+
+ context 'without FF `global_ci_catalog`' do
+ before do
+ stub_feature_flags(global_ci_catalog: false)
+ end
+
+ it 'responds with 404' do
+ get path
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+ end
+
+ describe 'GET #show' do
+ it_behaves_like 'basic get requests', :show
+ end
+
+ describe 'GET #index' do
+ it_behaves_like 'basic get requests', :index
+ end
+end
diff --git a/spec/requests/external_redirect/external_redirect_controller_spec.rb b/spec/requests/external_redirect/external_redirect_controller_spec.rb
new file mode 100644
index 00000000000..1b4294f5c4d
--- /dev/null
+++ b/spec/requests/external_redirect/external_redirect_controller_spec.rb
@@ -0,0 +1,60 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe "ExternalRedirect::ExternalRedirectController requests", feature_category: :navigation do
+ let_it_be(:external_url) { 'https://google.com' }
+ let_it_be(:external_url_encoded) do
+ Addressable::URI.encode_component(external_url, Addressable::URI::CharacterClasses::QUERY)
+ end
+
+ let_it_be(:internal_url) { "#{Gitlab.config.gitlab.url}/foo/bar" }
+ let_it_be(:internal_url_encoded) do
+ Addressable::URI.encode_component(internal_url, Addressable::URI::CharacterClasses::QUERY)
+ end
+
+ let_it_be(:top_nav_partial) { 'layouts/header/_default' }
+
+ context "with valid url param" do
+ before do
+ get "/-/external_redirect?url=#{external_url_encoded}"
+ end
+
+ it "returns 200 and renders URL" do
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response.body).to have_link(text: 'Proceed', href: external_url)
+ end
+
+ it "does not render nav" do
+ expect(response).not_to render_template(top_nav_partial)
+ end
+ end
+
+ context "with same origin url" do
+ before do
+ get "/-/external_redirect?url=#{internal_url_encoded}"
+ end
+
+ it "redirects" do
+ expect(response).to redirect_to(internal_url)
+ end
+ end
+
+ describe "with invalid url params" do
+ where(:case_name, :params) do
+ [
+ ["when url is bad", "url=javascript:alert(1)"],
+ ["when url is empty", "url="],
+ ["when url param is missing", ""]
+ ]
+ end
+
+ with_them do
+ it "returns 404" do
+ get "/-/external_redirect?#{params}"
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+ end
+end
diff --git a/spec/requests/health_controller_spec.rb b/spec/requests/health_controller_spec.rb
index 3ad1d8a75b4..639f6194af9 100644
--- a/spec/requests/health_controller_spec.rb
+++ b/spec/requests/health_controller_spec.rb
@@ -73,9 +73,7 @@ RSpec.describe HealthController, feature_category: :database do
end
describe 'GET /-/readiness' do
- subject(:request) { get readiness_path, params: params, headers: headers }
-
- it_behaves_like 'Base action controller'
+ subject { get '/-/readiness', params: params, headers: headers }
shared_context 'endpoint responding with readiness data' do
context 'when requesting instance-checks' do
diff --git a/spec/requests/jira_authorizations_spec.rb b/spec/requests/jira_authorizations_spec.rb
deleted file mode 100644
index 704db7fba08..00000000000
--- a/spec/requests/jira_authorizations_spec.rb
+++ /dev/null
@@ -1,88 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe 'Jira authorization requests', feature_category: :integrations do
- let(:user) { create :user }
- let(:application) { create :oauth_application, scopes: 'api' }
- let(:redirect_uri) { oauth_jira_dvcs_callback_url(host: "http://www.example.com") }
-
- def generate_access_grant
- create :oauth_access_grant, application: application, resource_owner_id: user.id, redirect_uri: redirect_uri
- end
-
- describe 'POST access_token' do
- let(:client_id) { application.uid }
- let(:client_secret) { application.secret }
-
- it 'returns values similar to a POST to /oauth/token' do
- post_data = {
- client_id: client_id,
- client_secret: client_secret
- }
-
- post '/oauth/token', params: post_data.merge({
- code: generate_access_grant.token,
- grant_type: 'authorization_code',
- redirect_uri: redirect_uri
- })
- oauth_response = json_response
- oauth_response_access_token, scope, token_type = oauth_response.values_at('access_token', 'scope', 'token_type')
-
- post '/login/oauth/access_token', params: post_data.merge({
- code: generate_access_grant.token
- })
- jira_response = response.body
- jira_response_access_token = Rack::Utils.parse_nested_query(jira_response)['access_token']
-
- expect(jira_response).to include("scope=#{scope}&token_type=#{token_type}")
- expect(oauth_response_access_token).not_to eql(jira_response_access_token)
- end
-
- it_behaves_like 'a GitHub Enterprise Jira DVCS reversible end of life endpoint' do
- subject do
- post '/login/oauth/access_token', params: {
- client_id: client_id,
- client_secret: client_secret,
- code: generate_access_grant.token
- }
- end
- end
-
- context 'when authorization fails' do
- before do
- post '/login/oauth/access_token', params: {
- client_id: client_id,
- client_secret: client_secret,
- code: try(:code) || generate_access_grant.token
- }
- end
-
- shared_examples 'an unauthorized request' do
- it 'returns 401' do
- expect(response).to have_gitlab_http_status(:unauthorized)
- end
- end
-
- context 'when client_id is invalid' do
- let(:client_id) { "invalid_id" }
-
- it_behaves_like 'an unauthorized request'
- end
-
- context 'when client_secret is invalid' do
- let(:client_secret) { "invalid_secret" }
-
- it_behaves_like 'an unauthorized request'
- end
-
- context 'when code is invalid' do
- let(:code) { "invalid_code" }
-
- it 'returns bad request' do
- expect(response).to have_gitlab_http_status(:bad_request)
- end
- end
- end
- end
-end
diff --git a/spec/requests/jwt_controller_spec.rb b/spec/requests/jwt_controller_spec.rb
index 965bead4068..966cc2d6d4e 100644
--- a/spec/requests/jwt_controller_spec.rb
+++ b/spec/requests/jwt_controller_spec.rb
@@ -61,7 +61,7 @@ RSpec.describe JwtController, feature_category: :system_access do
end
end
- shared_examples 'container registry authenticator' do
+ context 'authenticating against container registry' do
context 'existing service' do
subject! { get '/jwt/auth', params: parameters }
@@ -124,7 +124,7 @@ RSpec.describe JwtController, feature_category: :system_access do
end
it 'does not log a user' do
- expect(log_data.keys).not_to include(%w(username user_id))
+ expect(log_data.keys).not_to include(%w[username user_id])
end
end
@@ -177,7 +177,7 @@ RSpec.describe JwtController, feature_category: :system_access do
end
let(:service_parameters) do
- ActionController::Parameters.new({ service: service_name, scopes: %w(scope1 scope2) }).permit!
+ ActionController::Parameters.new({ service: service_name, scopes: %w[scope1 scope2] }).permit!
end
it { expect(service_class).to have_received(:new).with(nil, user, service_parameters.merge(auth_type: :gitlab_or_ldap)) }
@@ -185,6 +185,21 @@ RSpec.describe JwtController, feature_category: :system_access do
it_behaves_like 'user logging'
end
+ context 'when passing a space-delimited list of scopes' do
+ let(:parameters) do
+ {
+ service: service_name,
+ scope: 'scope1 scope2'
+ }
+ end
+
+ let(:service_parameters) do
+ ActionController::Parameters.new({ service: service_name, scopes: %w[scope1 scope2] }).permit!
+ end
+
+ it { expect(service_class).to have_received(:new).with(nil, user, service_parameters.merge(auth_type: :gitlab_or_ldap)) }
+ end
+
context 'when user has 2FA enabled' do
let(:user) { create(:user, :two_factor) }
@@ -254,40 +269,6 @@ RSpec.describe JwtController, feature_category: :system_access do
end
end
- shared_examples 'parses a space-delimited list of scopes' do |output|
- let(:user) { create(:user) }
- let(:headers) { { authorization: credentials(user.username, user.password) } }
-
- subject! { get '/jwt/auth', params: parameters, headers: headers }
-
- let(:parameters) do
- {
- service: service_name,
- scope: 'scope1 scope2'
- }
- end
-
- let(:service_parameters) do
- ActionController::Parameters.new({ service: service_name, scopes: output }).permit!
- end
-
- it { expect(service_class).to have_received(:new).with(nil, user, service_parameters.merge(auth_type: :gitlab_or_ldap)) }
- end
-
- context 'authenticating against container registry' do
- it_behaves_like 'container registry authenticator'
- it_behaves_like 'parses a space-delimited list of scopes', %w(scope1 scope2)
-
- context 'when jwt_auth_space_delimited_scopes feature flag is disabled' do
- before do
- stub_feature_flags(jwt_auth_space_delimited_scopes: false)
- end
-
- it_behaves_like 'container registry authenticator'
- it_behaves_like 'parses a space-delimited list of scopes', ['scope1 scope2']
- end
- end
-
context 'authenticating against dependency proxy' do
let_it_be(:user) { create(:user) }
let_it_be(:personal_access_token) { create(:personal_access_token, user: user) }
diff --git a/spec/requests/lfs_http_spec.rb b/spec/requests/lfs_http_spec.rb
index bc1ba3357a4..9bf77a0f6ca 100644
--- a/spec/requests/lfs_http_spec.rb
+++ b/spec/requests/lfs_http_spec.rb
@@ -664,8 +664,7 @@ RSpec.describe 'Git LFS API and storage', feature_category: :source_code_managem
context 'tries to push to other project' do
let(:pipeline) { create(:ci_empty_pipeline, project: other_project) }
- # I'm not sure what this tests that is different from the previous test
- it_behaves_like 'LFS http 403 response'
+ it_behaves_like 'LFS http 404 response'
end
end
@@ -1185,8 +1184,7 @@ RSpec.describe 'Git LFS API and storage', feature_category: :source_code_managem
context 'tries to push to other project' do
let(:pipeline) { create(:ci_empty_pipeline, project: other_project) }
- # I'm not sure what this tests that is different from the previous test
- it_behaves_like 'LFS http 403 response'
+ it_behaves_like 'LFS http 404 response'
end
end
diff --git a/spec/requests/lfs_locks_api_spec.rb b/spec/requests/lfs_locks_api_spec.rb
index 363a16f014b..a1a713308e0 100644
--- a/spec/requests/lfs_locks_api_spec.rb
+++ b/spec/requests/lfs_locks_api_spec.rb
@@ -67,7 +67,7 @@ RSpec.describe 'Git LFS File Locking API', feature_category: :source_code_manage
expect(response).to have_gitlab_http_status(:conflict)
- expect(json_response.keys).to match_array(%w(lock message documentation_url))
+ expect(json_response.keys).to match_array(%w[lock message documentation_url])
expect(json_response['message']).to match(/already locked/)
end
@@ -84,7 +84,7 @@ RSpec.describe 'Git LFS File Locking API', feature_category: :source_code_manage
expect(response).to have_gitlab_http_status(:created)
- expect(json_response['lock'].keys).to match_array(%w(id path locked_at owner))
+ expect(json_response['lock'].keys).to match_array(%w[id path locked_at owner])
end
end
end
@@ -103,7 +103,7 @@ RSpec.describe 'Git LFS File Locking API', feature_category: :source_code_manage
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['locks'].size).to eq(2)
- expect(json_response['locks'].first.keys).to match_array(%w(id path locked_at owner))
+ expect(json_response['locks'].first.keys).to match_array(%w[id path locked_at owner])
end
end
@@ -143,7 +143,7 @@ RSpec.describe 'Git LFS File Locking API', feature_category: :source_code_manage
it 'returns the deleted lock' do
post_lfs_json url, nil, headers
- expect(json_response['lock'].keys).to match_array(%w(id path locked_at owner))
+ expect(json_response['lock'].keys).to match_array(%w[id path locked_at owner])
end
context 'when a maintainer uses force' do
diff --git a/spec/requests/metrics_controller_spec.rb b/spec/requests/metrics_controller_spec.rb
deleted file mode 100644
index ce96906e020..00000000000
--- a/spec/requests/metrics_controller_spec.rb
+++ /dev/null
@@ -1,9 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe MetricsController, type: :request, feature_category: :metrics do
- it_behaves_like 'Base action controller' do
- subject(:request) { get metrics_path }
- end
-end
diff --git a/spec/requests/oauth/authorizations_controller_spec.rb b/spec/requests/oauth/authorizations_controller_spec.rb
index 7887bf52542..257f238d9ef 100644
--- a/spec/requests/oauth/authorizations_controller_spec.rb
+++ b/spec/requests/oauth/authorizations_controller_spec.rb
@@ -20,10 +20,6 @@ RSpec.describe Oauth::AuthorizationsController, feature_category: :system_access
end
describe 'GET #new' do
- it_behaves_like 'Base action controller' do
- subject(:request) { get oauth_authorization_path }
- end
-
context 'when application redirect URI has a custom scheme' do
context 'when CSP is disabled' do
before do
diff --git a/spec/requests/organizations/organizations_controller_spec.rb b/spec/requests/organizations/organizations_controller_spec.rb
index fdfeb367739..4bf527f49a8 100644
--- a/spec/requests/organizations/organizations_controller_spec.rb
+++ b/spec/requests/organizations/organizations_controller_spec.rb
@@ -75,6 +75,12 @@ RSpec.describe Organizations::OrganizationsController, feature_category: :cell d
it_behaves_like 'controller action that does not require authentication'
end
+ describe 'GET #users' do
+ subject(:gitlab_request) { get users_organization_path(organization) }
+
+ it_behaves_like 'controller action that does not require authentication'
+ end
+
describe 'GET #new' do
subject(:gitlab_request) { get new_organization_path }
diff --git a/spec/requests/profiles/comment_templates_controller_spec.rb b/spec/requests/profiles/comment_templates_controller_spec.rb
index cdbfbb0a346..d58fc3f19ea 100644
--- a/spec/requests/profiles/comment_templates_controller_spec.rb
+++ b/spec/requests/profiles/comment_templates_controller_spec.rb
@@ -10,26 +10,14 @@ RSpec.describe Profiles::CommentTemplatesController, feature_category: :user_pro
end
describe 'GET #index' do
- describe 'feature flag disabled' do
- before do
- stub_feature_flags(saved_replies: false)
-
- get '/-/profile/comment_templates'
- end
-
- it { expect(response).to have_gitlab_http_status(:not_found) }
+ before do
+ get '/-/profile/comment_templates'
end
- describe 'feature flag enabled' do
- before do
- get '/-/profile/comment_templates'
- end
-
- it { expect(response).to have_gitlab_http_status(:ok) }
+ it { expect(response).to have_gitlab_http_status(:ok) }
- it 'sets hide search settings ivar' do
- expect(assigns(:hide_search_settings)).to eq(true)
- end
+ it 'sets hide search settings ivar' do
+ expect(assigns(:hide_search_settings)).to eq(true)
end
end
end
diff --git a/spec/requests/projects/merge_requests_controller_spec.rb b/spec/requests/projects/merge_requests_controller_spec.rb
index 4af8f4fac7f..1033a51cd80 100644
--- a/spec/requests/projects/merge_requests_controller_spec.rb
+++ b/spec/requests/projects/merge_requests_controller_spec.rb
@@ -187,21 +187,6 @@ RSpec.describe Projects::MergeRequestsController, feature_category: :source_code
expect(response).to have_gitlab_http_status(:ok)
expect(Gitlab::Json.parse(response.body)['count']['all']).to eq(2)
end
-
- context 'when the FF ci_fix_performance_pipelines_json_endpoint is disabled' do
- before do
- stub_feature_flags(ci_fix_performance_pipelines_json_endpoint: false)
- end
-
- it 'returns the failed builds' do
- get pipelines_project_merge_request_path(project, merge_request, format: :json)
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(Gitlab::Json.parse(response.body)['pipelines'].size).to eq(1)
- expect(Gitlab::Json.parse(response.body)['pipelines'][0]['failed_builds_count']).to eq(2)
- expect(Gitlab::Json.parse(response.body)['pipelines'][0]['failed_builds'].size).to eq(2)
- end
- end
end
private
diff --git a/spec/requests/projects/ml/model_versions_controller_spec.rb b/spec/requests/projects/ml/model_versions_controller_spec.rb
new file mode 100644
index 00000000000..bd9d798c275
--- /dev/null
+++ b/spec/requests/projects/ml/model_versions_controller_spec.rb
@@ -0,0 +1,72 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Projects::Ml::ModelVersionsController, feature_category: :mlops do
+ let_it_be(:project) { create(:project) }
+ let_it_be(:another_project) { create(:project) }
+ let_it_be(:user) { project.first_owner }
+ let_it_be(:model) { create(:ml_models, :with_versions, project: project) }
+ let_it_be(:version) { model.versions.first }
+
+ let(:model_registry_enabled) { true }
+
+ before do
+ allow(Ability).to receive(:allowed?).and_call_original
+ allow(Ability).to receive(:allowed?)
+ .with(user, :read_model_registry, project)
+ .and_return(model_registry_enabled)
+
+ sign_in(user)
+ end
+
+ describe 'show' do
+ let(:model_id) { model.id }
+ let(:version_id) { version.id }
+ let(:request_project) { model.project }
+
+ subject(:show_request) do
+ show_model_version
+ response
+ end
+
+ before do
+ show_request
+ end
+
+ it 'renders the template' do
+ is_expected.to render_template('projects/ml/model_versions/show')
+ end
+
+ it 'fetches the correct model_version' do
+ show_request
+
+ expect(assigns(:model)).to eq(model)
+ expect(assigns(:model_version)).to eq(version)
+ end
+
+ context 'when version id does not exist' do
+ let(:version_id) { non_existing_record_id }
+
+ it { is_expected.to have_gitlab_http_status(:not_found) }
+ end
+
+ context 'when version and model id are correct but project is not' do
+ let(:request_project) { another_project }
+
+ it { is_expected.to have_gitlab_http_status(:not_found) }
+ end
+
+ context 'when user does not have access' do
+ let(:model_registry_enabled) { false }
+
+ it { is_expected.to have_gitlab_http_status(:not_found) }
+ end
+ end
+
+ private
+
+ def show_model_version
+ get project_ml_model_version_path(request_project, model_id, version_id)
+ end
+end
diff --git a/spec/requests/projects/ml/models_controller_spec.rb b/spec/requests/projects/ml/models_controller_spec.rb
index b4402ad9a27..cda3f777a72 100644
--- a/spec/requests/projects/ml/models_controller_spec.rb
+++ b/spec/requests/projects/ml/models_controller_spec.rb
@@ -10,14 +10,19 @@ RSpec.describe Projects::Ml::ModelsController, feature_category: :mlops do
let_it_be(:model3) { create(:ml_models, project: project) }
let_it_be(:model_in_different_project) { create(:ml_models) }
- let(:model_registry_enabled) { true }
+ let(:read_model_registry) { true }
+ let(:write_model_registry) { true }
+
let(:params) { {} }
before do
allow(Ability).to receive(:allowed?).and_call_original
allow(Ability).to receive(:allowed?)
.with(user, :read_model_registry, project)
- .and_return(model_registry_enabled)
+ .and_return(read_model_registry)
+ allow(Ability).to receive(:allowed?)
+ .with(user, :write_model_registry, project)
+ .and_return(write_model_registry)
sign_in(user)
end
@@ -33,34 +38,59 @@ RSpec.describe Projects::Ml::ModelsController, feature_category: :mlops do
end
it 'fetches the models using the finder' do
- expect(::Projects::Ml::ModelFinder).to receive(:new).with(project).and_call_original
+ expect(::Projects::Ml::ModelFinder).to receive(:new).with(project, {}).and_call_original
index_request
end
- it 'fetches the correct models' do
+ it 'fetches the correct variables', :aggregate_failures do
+ stub_const("Projects::Ml::ModelsController::MAX_MODELS_PER_PAGE", 2)
+
index_request
- expect(assigns(:paginator).records).to match_array([model3, model2, model1])
+ page_models = [model3, model2]
+ all_models = [model3, model2, model1]
+
+ expect(assigns(:paginator).records).to match_array(page_models)
+ expect(assigns(:model_count)).to be all_models.count
end
it 'does not perform N+1 sql queries' do
+ list_models
+
control_count = ActiveRecord::QueryRecorder.new(skip_cached: false) { list_models }
create_list(:ml_model_versions, 2, model: model1)
create_list(:ml_model_versions, 2, model: model2)
+ create_list(:ml_models, 4, project: project)
expect { list_models }.not_to exceed_all_query_limit(control_count)
end
context 'when user does not have access' do
- let(:model_registry_enabled) { false }
+ let(:read_model_registry) { false }
it 'renders 404' do
is_expected.to have_gitlab_http_status(:not_found)
end
end
+ context 'with search params' do
+ let(:params) { { name: 'some_name', order_by: 'name', sort: 'asc' } }
+
+ it 'passes down params to the finder' do
+ expect(Projects::Ml::ModelFinder).to receive(:new).and_call_original do |_exp, params|
+ expect(params.to_h).to include({
+ name: 'some_name',
+ order_by: 'name',
+ sort: 'asc'
+ })
+ end
+
+ index_request
+ end
+ end
+
describe 'pagination' do
before do
stub_const("Projects::Ml::ModelsController::MAX_MODELS_PER_PAGE", 2)
@@ -116,7 +146,40 @@ RSpec.describe Projects::Ml::ModelsController, feature_category: :mlops do
end
context 'when user does not have access' do
- let(:model_registry_enabled) { false }
+ let(:read_model_registry) { false }
+
+ it { is_expected.to have_gitlab_http_status(:not_found) }
+ end
+ end
+
+ describe 'destroy' do
+ let(:model_for_deletion) do
+ create(:ml_models, project: project)
+ end
+
+ let(:model_id) { model_for_deletion.id }
+
+ subject(:delete_request) do
+ delete_model
+ response
+ end
+
+ it 'deletes the model', :aggregate_failures do
+ is_expected.to have_gitlab_http_status(:found)
+
+ expect(flash[:notice]).to eq('Model removed')
+ expect(response).to redirect_to("/#{project.full_path}/-/ml/models")
+ expect { Ml::Model.find(id: model_id) }.to raise_error(ActiveRecord::RecordNotFound)
+ end
+
+ context 'when model does not exist' do
+ let(:model_id) { non_existing_record_id }
+
+ it { is_expected.to have_gitlab_http_status(:not_found) }
+ end
+
+ describe 'when user does not have write_model_registry rights' do
+ let(:write_model_registry) { false }
it { is_expected.to have_gitlab_http_status(:not_found) }
end
@@ -131,4 +194,8 @@ RSpec.describe Projects::Ml::ModelsController, feature_category: :mlops do
def show_model
get project_ml_model_path(request_project, model_id)
end
+
+ def delete_model
+ delete project_ml_model_path(project, model_id)
+ end
end
diff --git a/spec/requests/projects/service_desk_controller_spec.rb b/spec/requests/projects/service_desk_controller_spec.rb
index 05e48c2c5c7..7d881d8ea62 100644
--- a/spec/requests/projects/service_desk_controller_spec.rb
+++ b/spec/requests/projects/service_desk_controller_spec.rb
@@ -88,6 +88,16 @@ RSpec.describe Projects::ServiceDeskController, feature_category: :service_desk
expect(json_response['issue_template_key']).to eq('service_desk')
end
+ it 'sets add_external_participants_from_cc' do
+ put project_service_desk_path(project, format: :json), params: { add_external_participants_from_cc: true }
+ project.reset
+
+ settings = project.service_desk_setting
+ expect(settings).to be_present
+ expect(settings.add_external_participants_from_cc).to eq(true)
+ expect(json_response['add_external_participants_from_cc']).to eq(true)
+ end
+
it 'returns an error when update of service desk settings fails' do
put project_service_desk_path(project, format: :json), params: { issue_template_key: 'invalid key' }
diff --git a/spec/requests/registrations_controller_spec.rb b/spec/requests/registrations_controller_spec.rb
index 71f2f347f0d..8b857046a4d 100644
--- a/spec/requests/registrations_controller_spec.rb
+++ b/spec/requests/registrations_controller_spec.rb
@@ -6,9 +6,7 @@ RSpec.describe RegistrationsController, type: :request, feature_category: :syste
describe 'POST #create' do
let_it_be(:user_attrs) { build_stubbed(:user).slice(:first_name, :last_name, :username, :email, :password) }
- subject(:request) { post user_registration_path, params: { user: user_attrs } }
-
- it_behaves_like 'Base action controller'
+ subject(:create_user) { post user_registration_path, params: { user: user_attrs } }
context 'when email confirmation is required' do
before do
@@ -17,7 +15,7 @@ RSpec.describe RegistrationsController, type: :request, feature_category: :syste
end
it 'redirects to the `users_almost_there_path`', unless: Gitlab.ee? do
- request
+ create_user
expect(response).to redirect_to(users_almost_there_path(email: user_attrs[:email]))
end
diff --git a/spec/requests/search_controller_spec.rb b/spec/requests/search_controller_spec.rb
index 37474aee1ee..365b20ad4aa 100644
--- a/spec/requests/search_controller_spec.rb
+++ b/spec/requests/search_controller_spec.rb
@@ -9,7 +9,6 @@ RSpec.describe SearchController, type: :request, feature_category: :global_searc
let_it_be(:projects) { create_list(:project, 5, :public, :repository, :wiki_repo) }
before do
- stub_feature_flags(super_sidebar_nav_enrolled: false)
login_as(user)
end
diff --git a/spec/requests/sessions_spec.rb b/spec/requests/sessions_spec.rb
index 1a925969c5a..3428e607305 100644
--- a/spec/requests/sessions_spec.rb
+++ b/spec/requests/sessions_spec.rb
@@ -7,10 +7,6 @@ RSpec.describe 'Sessions', feature_category: :system_access do
let(:user) { create(:user) }
- it_behaves_like 'Base action controller' do
- subject(:request) { get new_user_session_path }
- end
-
context 'for authentication', :allow_forgery_protection do
it 'logout does not require a csrf token' do
login_as(user)
diff --git a/spec/requests/users_controller_spec.rb b/spec/requests/users_controller_spec.rb
index d4e7dc1542a..da111831c15 100644
--- a/spec/requests/users_controller_spec.rb
+++ b/spec/requests/users_controller_spec.rb
@@ -600,7 +600,7 @@ RSpec.describe UsersController, feature_category: :user_management do
end
end
- %i(html json).each do |format|
+ %i[html json].each do |format|
context "with format: #{format}" do
let(:format) { format }
@@ -656,7 +656,7 @@ RSpec.describe UsersController, feature_category: :user_management do
end
end
- %i(html json).each do |format|
+ %i[html json].each do |format|
context "with format: #{format}" do
let(:format) { format }
diff --git a/spec/routing/organizations/organizations_controller_routing_spec.rb b/spec/routing/organizations/organizations_controller_routing_spec.rb
index 187553df2a1..f105bb31ccf 100644
--- a/spec/routing/organizations/organizations_controller_routing_spec.rb
+++ b/spec/routing/organizations/organizations_controller_routing_spec.rb
@@ -24,4 +24,9 @@ RSpec.describe Organizations::OrganizationsController, :routing, feature_categor
expect(get("/-/organizations/#{organization.path}/groups_and_projects"))
.to route_to('organizations/organizations#groups_and_projects', organization_path: organization.path)
end
+
+ it 'routes to #users' do
+ expect(get("/-/organizations/#{organization.path}/users"))
+ .to route_to('organizations/organizations#users', organization_path: organization.path)
+ end
end
diff --git a/spec/routing/uploads_routing_spec.rb b/spec/routing/uploads_routing_spec.rb
index 9eb421ec7d0..198cda59357 100644
--- a/spec/routing/uploads_routing_spec.rb
+++ b/spec/routing/uploads_routing_spec.rb
@@ -33,7 +33,7 @@ RSpec.describe 'Uploads', 'routing' do
end
it 'does not allow creating uploads for other models' do
- unroutable_models = UploadsController::MODEL_CLASSES.keys.compact - %w(personal_snippet user)
+ unroutable_models = UploadsController::MODEL_CLASSES.keys.compact - %w[personal_snippet user]
unroutable_models.each do |model|
expect(post("/uploads/#{model}?id=1")).not_to be_routable
diff --git a/spec/rubocop/batched_background_migrations_spec.rb b/spec/rubocop/batched_background_migrations_dictionary_spec.rb
index a9b99bb466b..57ef929fd90 100644
--- a/spec/rubocop/batched_background_migrations_spec.rb
+++ b/spec/rubocop/batched_background_migrations_dictionary_spec.rb
@@ -2,20 +2,24 @@
require 'rubocop_spec_helper'
-require_relative '../../rubocop/batched_background_migrations'
+require_relative '../../rubocop/batched_background_migrations_dictionary'
-RSpec.describe RuboCop::BatchedBackgroundMigrations, feature_category: :database do
+RSpec.describe RuboCop::BatchedBackgroundMigrationsDictionary, feature_category: :database do
let(:bbm_dictionary_file_name) { "#{described_class::DICTIONARY_BASE_DIR}/test_migration.yml" }
let(:migration_version) { 20230307160250 }
let(:finalized_by_version) { 20230307160255 }
+ let(:introduced_by_url) { 'https://test_url' }
+ let(:finalize_after) { '202312011212' }
+
let(:bbm_dictionary_data) do
{
migration_job_name: 'TestMigration',
feature_category: :database,
- introduced_by_url: 'https://test_url',
+ introduced_by_url: introduced_by_url,
milestone: 16.5,
queued_migration_version: migration_version,
- finalized_by: finalized_by_version
+ finalized_by: finalized_by_version,
+ finalize_after: finalize_after
}
end
@@ -40,4 +44,24 @@ RSpec.describe RuboCop::BatchedBackgroundMigrations, feature_category: :database
expect(described_class.new('random').finalized_by).to be_nil
end
end
+
+ describe '#introduced_by_url' do
+ it 'returns the introduced_by_url of the bbm with given version' do
+ expect(batched_background_migration.introduced_by_url).to eq(introduced_by_url)
+ end
+
+ it 'returns nothing for non-existing bbm dictionary' do
+ expect(described_class.new('random').introduced_by_url).to be_nil
+ end
+ end
+
+ describe '#finalize_after' do
+ it 'returns the finalize_after timestamp of the bbm with given version' do
+ expect(batched_background_migration.finalize_after).to eq(finalize_after)
+ end
+
+ it 'returns nothing for non-existing bbm dictionary' do
+ expect(described_class.new('random').finalize_after).to be_nil
+ end
+ end
end
diff --git a/spec/rubocop/cop/background_migration/missing_dictionary_file_spec.rb b/spec/rubocop/cop/background_migration/dictionary_file_spec.rb
index 32b958426b9..7becf9c09a4 100644
--- a/spec/rubocop/cop/background_migration/missing_dictionary_file_spec.rb
+++ b/spec/rubocop/cop/background_migration/dictionary_file_spec.rb
@@ -1,17 +1,36 @@
# frozen_string_literal: true
require 'rubocop_spec_helper'
-require_relative '../../../../rubocop/cop/background_migration/missing_dictionary_file'
+require_relative '../../../../rubocop/cop/background_migration/dictionary_file'
-RSpec.describe RuboCop::Cop::BackgroundMigration::MissingDictionaryFile, feature_category: :database do
+RSpec.describe RuboCop::Cop::BackgroundMigration::DictionaryFile, feature_category: :database do
let(:config) do
RuboCop::Config.new(
- 'BackgroundMigration/MissingDictionaryFile' => {
- 'EnforcedSince' => 20230307160251
+ 'BackgroundMigration/DictionaryFile' => {
+ 'EnforcedSince' => 20231018100907
}
)
end
+ shared_examples 'migration with missing dictionary keys offense' do |missing_key|
+ it 'registers an offense' do
+ expect_offense(<<~RUBY)
+ class QueueMyMigration < Gitlab::Database::Migration[2.1]
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ #{format(described_class::MSG[:missing_key], key: missing_key)}
+ MIGRATION = 'MyMigration'
+
+ def up
+ queue_batched_background_migration(
+ MIGRATION,
+ :users,
+ :id
+ )
+ end
+ end
+ RUBY
+ end
+ end
+
context 'for non post migrations' do
before do
allow(cop).to receive(:in_post_deployment_migration?).and_return(false)
@@ -57,7 +76,7 @@ RSpec.describe RuboCop::Cop::BackgroundMigration::MissingDictionaryFile, feature
context 'for migrations before enforced time' do
before do
- allow(cop).to receive(:version).and_return(20230307160250)
+ allow(cop).to receive(:version).and_return(20230918100907)
end
it 'does not throw any offenses' do
@@ -79,7 +98,7 @@ RSpec.describe RuboCop::Cop::BackgroundMigration::MissingDictionaryFile, feature
context 'for migrations after enforced time' do
before do
- allow(cop).to receive(:version).and_return(20230307160252)
+ allow(cop).to receive(:version).and_return(20231118100907)
end
it 'throws offense on not having the appropriate dictionary file with migration name as a constant' do
@@ -114,22 +133,48 @@ RSpec.describe RuboCop::Cop::BackgroundMigration::MissingDictionaryFile, feature
RUBY
end
- it 'does not throw offense with appropriate dictionary file' do
- expect(File).to receive(:exist?).with(dictionary_file_path).and_return(true)
+ context 'with dictionary file' do
+ let(:introduced_by_url) { 'https://test_url' }
+ let(:finalize_after) { '20230507160251' }
- expect_no_offenses(<<~RUBY)
- class QueueMyMigration < Gitlab::Database::Migration[2.1]
- MIGRATION = 'MyMigration'
+ before do
+ allow(File).to receive(:exist?).with(dictionary_file_path).and_return(true)
- def up
- queue_batched_background_migration(
- MIGRATION,
- :users,
- :id
- )
- end
+ allow_next_instance_of(RuboCop::BatchedBackgroundMigrationsDictionary) do |dictionary|
+ allow(dictionary).to receive(:finalize_after).and_return(finalize_after)
+ allow(dictionary).to receive(:introduced_by_url).and_return(introduced_by_url)
end
- RUBY
+ end
+
+ context 'without introduced_by_url' do
+ it_behaves_like 'migration with missing dictionary keys offense', :introduced_by_url do
+ let(:introduced_by_url) { nil }
+ end
+ end
+
+ context 'without finalize_after' do
+ it_behaves_like 'migration with missing dictionary keys offense', :finalize_after do
+ let(:finalize_after) { nil }
+ end
+ end
+
+ context 'with required dictionary keys' do
+ it 'does not throw offense with appropriate dictionary file' do
+ expect_no_offenses(<<~RUBY)
+ class QueueMyMigration < Gitlab::Database::Migration[2.1]
+ MIGRATION = 'MyMigration'
+
+ def up
+ queue_batched_background_migration(
+ MIGRATION,
+ :users,
+ :id
+ )
+ end
+ end
+ RUBY
+ end
+ end
end
end
end
diff --git a/spec/rubocop/cop/gitlab/doc_url_spec.rb b/spec/rubocop/cop/gitlab/doc_url_spec.rb
index 957edc8286b..fa055f9b2fe 100644
--- a/spec/rubocop/cop/gitlab/doc_url_spec.rb
+++ b/spec/rubocop/cop/gitlab/doc_url_spec.rb
@@ -9,7 +9,7 @@ RSpec.describe RuboCop::Cop::Gitlab::DocUrl, feature_category: :shared do
it 'registers an offense' do
expect_offense(<<~RUBY)
'See [the docs](https://docs.gitlab.com/ee/user/permissions#roles).'
- ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Use `#help_page_url` instead of directly including link. See https://docs.gitlab.com/ee/development/documentation/#linking-to-help-in-ruby.
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Use `#help_page_url` instead of directly including link. See [...]
RUBY
end
end
@@ -19,7 +19,7 @@ RSpec.describe RuboCop::Cop::Gitlab::DocUrl, feature_category: :shared do
expect_offense(<<~'RUBY')
'See the docs: ' \
'https://docs.gitlab.com/ee/user/permissions#roles'
- ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Use `#help_page_url` instead of directly including link. See https://docs.gitlab.com/ee/development/documentation/#linking-to-help-in-ruby.
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Use `#help_page_url` instead of directly including link. See [...]
RUBY
end
end
@@ -30,7 +30,7 @@ RSpec.describe RuboCop::Cop::Gitlab::DocUrl, feature_category: :shared do
<<-HEREDOC
See the docs:
https://docs.gitlab.com/ee/user/permissions#roles
- ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Use `#help_page_url` instead of directly including link. See https://docs.gitlab.com/ee/development/documentation/#linking-to-help-in-ruby.
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Use `#help_page_url` instead of directly including link. See [...]
HEREDOC
RUBY
end
diff --git a/spec/rubocop/cop/gitlab/feature_available_usage_spec.rb b/spec/rubocop/cop/gitlab/feature_available_usage_spec.rb
index 184f2c3ee92..263cd8244b0 100644
--- a/spec/rubocop/cop/gitlab/feature_available_usage_spec.rb
+++ b/spec/rubocop/cop/gitlab/feature_available_usage_spec.rb
@@ -17,7 +17,7 @@ RSpec.describe RuboCop::Cop::Gitlab::FeatureAvailableUsage do
end
it 'does not flag the use of Gitlab::Saas.feature_available?' do
- expect_no_offenses('Gitlab::Saas.feature_available?("some/feature")')
+ expect_no_offenses('Gitlab::Saas.feature_available?(:some_feature)')
end
it 'flags the use with a dynamic feature as nil' do
diff --git a/spec/rubocop/cop/gitlab/mark_used_feature_flags_spec.rb b/spec/rubocop/cop/gitlab/mark_used_feature_flags_spec.rb
index 4b7ea6b72e5..8d80a554a2a 100644
--- a/spec/rubocop/cop/gitlab/mark_used_feature_flags_spec.rb
+++ b/spec/rubocop/cop/gitlab/mark_used_feature_flags_spec.rb
@@ -146,40 +146,6 @@ RSpec.describe RuboCop::Cop::Gitlab::MarkUsedFeatureFlags do
end
end
- %w[
- use_rugged?
- ].each do |feature_flag_method|
- context "#{feature_flag_method} method" do
- context 'a string feature flag' do
- include_examples 'sets flag as used', %|#{feature_flag_method}(arg, "baz")|, 'baz'
- end
-
- context 'a symbol feature flag' do
- include_examples 'sets flag as used', %|#{feature_flag_method}(arg, :baz)|, 'baz'
- end
-
- context 'an interpolated string feature flag with a string prefix' do
- include_examples 'sets flag as used', %|#{feature_flag_method}(arg, "foo_\#{bar}")|, %w[foo_hello foo_world]
- end
-
- context 'an interpolated symbol feature flag with a string prefix' do
- include_examples 'sets flag as used', %|#{feature_flag_method}(arg, :"foo_\#{bar}")|, %w[foo_hello foo_world]
- end
-
- context 'an interpolated string feature flag with a string prefix and suffix' do
- include_examples 'does not set any flags as used', %|#{feature_flag_method}(arg, :"foo_\#{bar}_baz")|
- end
-
- context 'a dynamic string feature flag as a variable' do
- include_examples 'does not set any flags as used', %|#{feature_flag_method}(a_variable, an_arg)|
- end
-
- context 'an integer feature flag' do
- include_examples 'does not set any flags as used', %|#{feature_flag_method}(arg, 123)|
- end
- end
- end
-
describe 'self.limit_feature_flag = :foo' do
include_examples 'sets flag as used', 'self.limit_feature_flag = :foo', 'foo'
end
diff --git a/spec/rubocop/cop/migration/migration_record_spec.rb b/spec/rubocop/cop/migration/migration_record_spec.rb
index 96a1d8fa107..0294e6c43ab 100644
--- a/spec/rubocop/cop/migration/migration_record_spec.rb
+++ b/spec/rubocop/cop/migration/migration_record_spec.rb
@@ -15,7 +15,7 @@ RSpec.describe RuboCop::Cop::Migration::MigrationRecord do
end
end
- %w(ActiveRecord::Base ApplicationRecord).each do |klass|
+ %w[ActiveRecord::Base ApplicationRecord].each do |klass|
context 'outside of a migration' do
it_behaves_like 'a disabled cop', klass
end
diff --git a/spec/rubocop/cop/migration/migration_with_milestone_spec.rb b/spec/rubocop/cop/migration/migration_with_milestone_spec.rb
new file mode 100644
index 00000000000..dd603cad2f8
--- /dev/null
+++ b/spec/rubocop/cop/migration/migration_with_milestone_spec.rb
@@ -0,0 +1,99 @@
+# frozen_string_literal: true
+
+require 'rubocop_spec_helper'
+require_relative '../../../../rubocop/cop/migration/migration_with_milestone'
+
+RSpec.describe RuboCop::Cop::Migration::MigrationWithMilestone, feature_category: :database do
+ context 'when we\'re not a Gitlab migration' do
+ it 'does not register an offense at all' do
+ expect_no_offenses <<~CODE
+ class CreateProducts < ActiveRecord::Migration[7.0]
+ def change
+ add_column :users, :foo, :integer
+ end
+ end
+ CODE
+ end
+ end
+
+ context 'when we\'re a Gitlab migration' do
+ it 'does not register an offense if we\'re a version before 2.2' do
+ expect_no_offenses <<~CODE
+ class TestFoo < Gitlab::Database::Migration[2.1]
+ def change
+ add_column :users, :foo, :integer
+ end
+ end
+ CODE
+ end
+
+ context 'when we\'re version 2.2' do
+ it 'expects no offense if we call `milestone` with a string' do
+ expect_no_offenses <<~CODE
+ class TestFoo < Gitlab::Database::Migration[2.2]
+ milestone '16.7'
+
+ def change
+ add_column :users, :foo, :integer
+ end
+ end
+ CODE
+ end
+
+ it 'expects an offense if we don\'t call `milestone`' do
+ expect_offense <<~CODE
+ class TestFoo < Gitlab::Database::Migration[2.2]
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ #{described_class::MSG}
+ def change
+ add_column :users, :foo, :integer
+ end
+ end
+ CODE
+ end
+
+ it 'does not matter if include a mixin' do
+ expect_no_offenses <<~CODE
+ class TestFoo < Gitlab::Database::Migration[2.2]
+ include Gitlab::Test::Mixin
+
+ milestone '16.7'
+
+ def change
+ add_column :users, :foo, :integer
+ end
+ end
+ CODE
+ end
+
+ it 'does not matter if we call a helper method' do
+ expect_no_offenses <<~CODE
+ class TestFoo < Gitlab::Database::Migration[2.2]
+ disable_ddl_transaction!
+
+ milestone '16.7'
+
+ def change
+ add_column :users, :foo, :integer
+ end
+ end
+ CODE
+ end
+
+ it 'does not matter if we include a mixin and call a helper method' do
+ expect_no_offenses <<~CODE
+ class TestFoo < Gitlab::Database::Migration[2.2]
+ include Gitlab::Test::Mixin
+
+ disable_ddl_transaction!
+
+ milestone '16.7'
+
+ def change
+ add_column :users, :foo, :integer
+ end
+ end
+ CODE
+ end
+ end
+ end
+end
diff --git a/spec/rubocop/cop/migration/prevent_index_creation_spec.rb b/spec/rubocop/cop/migration/prevent_index_creation_spec.rb
index 088edfedfc9..5e7a1bd100d 100644
--- a/spec/rubocop/cop/migration/prevent_index_creation_spec.rb
+++ b/spec/rubocop/cop/migration/prevent_index_creation_spec.rb
@@ -4,7 +4,7 @@ require 'rubocop_spec_helper'
require_relative '../../../../rubocop/cop/migration/prevent_index_creation'
RSpec.describe RuboCop::Cop::Migration::PreventIndexCreation do
- let(:forbidden_tables) { %w(ci_builds namespaces) }
+ let(:forbidden_tables) { %w[ci_builds namespaces projects users] }
let(:forbidden_tables_list) { forbidden_tables.join(', ') }
context 'when in migration' do
diff --git a/spec/rubocop/cop/migration/sidekiq_queue_migrate_spec.rb b/spec/rubocop/cop/migration/sidekiq_queue_migrate_spec.rb
index 46c460b5d49..675df0318a9 100644
--- a/spec/rubocop/cop/migration/sidekiq_queue_migrate_spec.rb
+++ b/spec/rubocop/cop/migration/sidekiq_queue_migrate_spec.rb
@@ -14,7 +14,7 @@ RSpec.describe RuboCop::Cop::Migration::SidekiqQueueMigrate do
allow(cop).to receive(:in_post_deployment_migration?).and_return(false)
end
- %w(up down change any_other_method).each do |method_name|
+ %w[up down change any_other_method].each do |method_name|
it "registers an offense when sidekiq_queue_migrate is used in ##{method_name}" do
expect_offense(<<~RUBY)
def #{method_name}
diff --git a/spec/rubocop/cop/migration/unfinished_dependencies_spec.rb b/spec/rubocop/cop/migration/unfinished_dependencies_spec.rb
index cac48871856..f2e963ad322 100644
--- a/spec/rubocop/cop/migration/unfinished_dependencies_spec.rb
+++ b/spec/rubocop/cop/migration/unfinished_dependencies_spec.rb
@@ -99,7 +99,7 @@ RSpec.describe RuboCop::Cop::Migration::UnfinishedDependencies, feature_category
context 'with properly finalized dependent background migrations' do
before do
- allow_next_instance_of(RuboCop::BatchedBackgroundMigrations) do |bbms|
+ allow_next_instance_of(RuboCop::BatchedBackgroundMigrationsDictionary) do |bbms|
allow(bbms).to receive(:finalized_by).and_return(version - 5)
end
end
diff --git a/spec/rubocop/cop/migration/with_lock_retries_disallowed_method_spec.rb b/spec/rubocop/cop/migration/with_lock_retries_disallowed_method_spec.rb
index 5762f78820c..c132237a8c4 100644
--- a/spec/rubocop/cop/migration/with_lock_retries_disallowed_method_spec.rb
+++ b/spec/rubocop/cop/migration/with_lock_retries_disallowed_method_spec.rb
@@ -3,7 +3,7 @@
require 'rubocop_spec_helper'
require_relative '../../../../rubocop/cop/migration/with_lock_retries_disallowed_method'
-RSpec.describe RuboCop::Cop::Migration::WithLockRetriesDisallowedMethod do
+RSpec.describe RuboCop::Cop::Migration::WithLockRetriesDisallowedMethod, feature_category: :database do
context 'when in migration' do
before do
allow(cop).to receive(:in_migration?).and_return(true)
diff --git a/spec/rubocop/cop/performance/readlines_each_spec.rb b/spec/rubocop/cop/performance/readlines_each_spec.rb
index 11e2cee9262..829dd72fa10 100644
--- a/spec/rubocop/cop/performance/readlines_each_spec.rb
+++ b/spec/rubocop/cop/performance/readlines_each_spec.rb
@@ -28,7 +28,7 @@ RSpec.describe RuboCop::Cop::Performance::ReadlinesEach do
end
context 'when reading all lines using IO.readlines.each' do
- %w(IO File).each do |klass|
+ %w[IO File].each do |klass|
it_behaves_like('class read', klass)
end
diff --git a/spec/rubocop/cop/style/inline_disable_annotation_spec.rb b/spec/rubocop/cop/style/inline_disable_annotation_spec.rb
new file mode 100644
index 00000000000..a180c08d534
--- /dev/null
+++ b/spec/rubocop/cop/style/inline_disable_annotation_spec.rb
@@ -0,0 +1,46 @@
+# frozen_string_literal: true
+
+require 'rubocop_spec_helper'
+
+require_relative '../../../../rubocop/cop/style/inline_disable_annotation'
+
+RSpec.describe RuboCop::Cop::Style::InlineDisableAnnotation, feature_category: :shared do
+ it 'registers an offense' do
+ expect_offense(<<~RUBY)
+ # some other comment
+ abc = '1'
+ ['this', 'that'].each do |word|
+ next if something? # rubocop:disable Some/Cop, Another/Cop
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Inline disabling a cop needs to follow [...]
+ end
+ # rubocop:disable Some/Cop, Another/Cop - Bad comment
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Inline disabling a cop needs to follow [...]
+ # rubocop :todo Some/Cop Some other things
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Inline disabling a cop needs to follow [...]
+ # rubocop: disable Some/Cop, Another/Cop Some more stuff
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Inline disabling a cop needs to follow [...]
+ # rubocop:disable Some/Cop -- Good comment
+ if blah && this # some other comment about nothing
+ this.match?(/blah/) # rubocop:disable Some/Cop with a bad comment
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Inline disabling a cop needs to follow [...]
+ end
+ RUBY
+ end
+
+ it 'accepts correctly formatted comment' do
+ expect_no_offenses(<<~RUBY)
+ # some other comment
+ abc = '1'
+ ['this', 'that'].each do |word|
+ next if something? # rubocop:disable Some/Cop, Another/Cop -- Good comment
+ end
+ # rubocop:disable Some/Cop, Another/Cop -- Good comment
+ # rubocop :todo Some/Cop Some other things -- Good comment
+ # rubocop: disable Some/Cop, Another/Cop Some more stuff -- Good comment
+ # rubocop:disable Some/Cop -- Good comment
+ if blah && this # some other comment about nothing
+ this.match?(/blah/) # rubocop:disable Some/Cop -- Good comment
+ end
+ RUBY
+ end
+end
diff --git a/spec/rubocop/rubocop_spec.rb b/spec/rubocop/rubocop_spec.rb
new file mode 100644
index 00000000000..a80a0f72bdf
--- /dev/null
+++ b/spec/rubocop/rubocop_spec.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+# No spec helper is `require`d because `fast_spec_helper` requires
+# `active_support/all` and we want to ensure that `rubocop/rubocop` loads it.
+
+require 'rubocop'
+require_relative '../../rubocop/rubocop'
+
+RSpec.describe 'rubocop/rubocop', feature_category: :tooling do
+ it 'loads activesupport to enhance Enumerable' do
+ expect(Enumerable.instance_methods).to include(:exclude?)
+ end
+end
diff --git a/spec/scripts/lib/glfm/update_specification_spec.rb b/spec/scripts/lib/glfm/update_specification_spec.rb
index 92434b37515..500e8685e77 100644
--- a/spec/scripts/lib/glfm/update_specification_spec.rb
+++ b/spec/scripts/lib/glfm/update_specification_spec.rb
@@ -278,7 +278,7 @@ RSpec.describe Glfm::UpdateSpecification, '#process', feature_category: :team_pl
end
it 'includes header and all examples', :unlimited_max_formatted_output_length do
- # rubocop:disable Style/StringConcatenation (string contatenation is more readable)
+ # rubocop:disable Style/StringConcatenation -- string contatenation is more readable
expected = described_class::ES_SNAPSHOT_SPEC_MD_HEADER +
ghfm_spec_txt_examples +
"\n" +
diff --git a/spec/serializers/build_details_entity_spec.rb b/spec/serializers/build_details_entity_spec.rb
index a9d58b20861..874bcbfceaf 100644
--- a/spec/serializers/build_details_entity_spec.rb
+++ b/spec/serializers/build_details_entity_spec.rb
@@ -128,7 +128,7 @@ RSpec.describe BuildDetailsEntity do
context 'when the dependency is in the same pipeline' do
let!(:test1) { create(:ci_build, :success, :expired, pipeline: pipeline, name: 'test1', stage_idx: 0) }
let!(:test2) { create(:ci_build, :success, :expired, pipeline: pipeline, name: 'test2', stage_idx: 1) }
- let!(:build) { create(:ci_build, :pending, pipeline: pipeline, stage_idx: 2, options: { dependencies: %w(test1 test2) }) }
+ let!(:build) { create(:ci_build, :pending, pipeline: pipeline, stage_idx: 2, options: { dependencies: %w[test1 test2] }) }
before do
build.pipeline.unlocked!
@@ -148,7 +148,7 @@ RSpec.describe BuildDetailsEntity do
end
context 'when dependency is not found' do
- let!(:build) { create(:ci_build, :pending, pipeline: pipeline, stage_idx: 2, options: { dependencies: %w(test1 test2) }) }
+ let!(:build) { create(:ci_build, :pending, pipeline: pipeline, stage_idx: 2, options: { dependencies: %w[test1 test2] }) }
before do
build.pipeline.unlocked!
diff --git a/spec/serializers/ci/job_entity_spec.rb b/spec/serializers/ci/job_entity_spec.rb
index 6dce87a1fc5..c3d0de11405 100644
--- a/spec/serializers/ci/job_entity_spec.rb
+++ b/spec/serializers/ci/job_entity_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Ci::JobEntity do
+RSpec.describe Ci::JobEntity, feature_category: :continuous_integration do
let(:user) { create(:user) }
let(:job) { create(:ci_build, :running) }
let(:project) { job.project }
diff --git a/spec/serializers/ci/pipeline_entity_spec.rb b/spec/serializers/ci/pipeline_entity_spec.rb
index 0fd9a12440f..e4ac8488c8c 100644
--- a/spec/serializers/ci/pipeline_entity_spec.rb
+++ b/spec/serializers/ci/pipeline_entity_spec.rb
@@ -256,7 +256,6 @@ RSpec.describe Ci::PipelineEntity, feature_category: :continuous_integration do
project.add_maintainer(user)
end
- # Remove with `ci_fix_performance_pipelines_json_endpoint`.
context 'when disable_failed_builds is true' do
let(:options) { { disable_failed_builds: true } }
diff --git a/spec/serializers/container_repositories_serializer_spec.rb b/spec/serializers/container_repositories_serializer_spec.rb
index a0d08a8ba44..4ada143cb9c 100644
--- a/spec/serializers/container_repositories_serializer_spec.rb
+++ b/spec/serializers/container_repositories_serializer_spec.rb
@@ -12,7 +12,7 @@ RSpec.describe ContainerRepositoriesSerializer do
project.add_developer(user)
stub_container_registry_config(enabled: true)
- stub_container_registry_tags(repository: /image/, tags: %w(rootA latest))
+ stub_container_registry_tags(repository: /image/, tags: %w[rootA latest])
end
describe '#represent' do
diff --git a/spec/serializers/diff_file_entity_spec.rb b/spec/serializers/diff_file_entity_spec.rb
index 5eee9c34e1e..00083dd501a 100644
--- a/spec/serializers/diff_file_entity_spec.rb
+++ b/spec/serializers/diff_file_entity_spec.rb
@@ -28,7 +28,7 @@ RSpec.describe DiffFileEntity do
let(:code_navigation_path) { Gitlab::CodeNavigationPath.new(project, project.commit.sha) }
let(:request) { EntityRequest.new(project: project, current_user: user) }
let(:entity) { described_class.new(diff_file, options.merge(request: request, merge_request: merge_request, code_navigation_path: code_navigation_path)) }
- let(:exposed_urls) { %i(edit_path view_path context_lines_path) }
+ let(:exposed_urls) { %i[edit_path view_path context_lines_path] }
it_behaves_like 'diff file entity'
diff --git a/spec/serializers/group_child_entity_spec.rb b/spec/serializers/group_child_entity_spec.rb
index 5af704a42da..e0d86377e18 100644
--- a/spec/serializers/group_child_entity_spec.rb
+++ b/spec/serializers/group_child_entity_spec.rb
@@ -144,13 +144,13 @@ RSpec.describe GroupChildEntity do
end
it 'includes the counts' do
- expect(json.keys).to include(*%i(project_count subgroup_count))
+ expect(json.keys).to include(*%i[project_count subgroup_count])
end
end
describe 'user is not a member of the group' do
it 'does not include the counts' do
- expect(json.keys).not_to include(*%i(project_count subgroup_count))
+ expect(json.keys).not_to include(*%i[project_count subgroup_count])
end
end
@@ -162,7 +162,7 @@ RSpec.describe GroupChildEntity do
end
it 'does not include the counts' do
- expect(json.keys).not_to include(*%i(project_count subgroup_count))
+ expect(json.keys).not_to include(*%i[project_count subgroup_count])
end
end
end
diff --git a/spec/serializers/group_link/group_group_link_entity_spec.rb b/spec/serializers/group_link/group_group_link_entity_spec.rb
index 502cdc5c048..8f31c53e841 100644
--- a/spec/serializers/group_link/group_group_link_entity_spec.rb
+++ b/spec/serializers/group_link/group_group_link_entity_spec.rb
@@ -9,8 +9,8 @@ RSpec.describe GroupLink::GroupGroupLinkEntity do
let(:entity) { described_class.new(group_group_link, { current_user: current_user, source: shared_group }) }
- before do
- allow(entity).to receive(:current_user).and_return(current_user)
+ subject(:as_json) do
+ entity.as_json
end
it 'matches json schema' do
@@ -19,7 +19,7 @@ RSpec.describe GroupLink::GroupGroupLinkEntity do
context 'source' do
it 'exposes `source`' do
- expect(entity.as_json[:source]).to include(
+ expect(as_json[:source]).to include(
id: shared_group.id,
full_name: shared_group.full_name,
web_url: shared_group.web_url
@@ -38,9 +38,9 @@ RSpec.describe GroupLink::GroupGroupLinkEntity do
end
end
- context 'when current user has `:admin_group_member` permissions' do
- before do
- allow(entity).to receive(:can?).with(current_user, :admin_group_member, shared_group).and_return(true)
+ context 'when current user has owner permissions for the shared group' do
+ before_all do
+ shared_group.add_owner(current_user)
end
context 'when direct_member? is true' do
@@ -49,10 +49,8 @@ RSpec.describe GroupLink::GroupGroupLinkEntity do
end
it 'exposes `can_update` and `can_remove` as `true`' do
- json = entity.as_json
-
- expect(json[:can_update]).to be true
- expect(json[:can_remove]).to be true
+ expect(as_json[:can_update]).to be true
+ expect(as_json[:can_remove]).to be true
end
end
@@ -62,10 +60,51 @@ RSpec.describe GroupLink::GroupGroupLinkEntity do
end
it 'exposes `can_update` and `can_remove` as `true`' do
- json = entity.as_json
+ expect(as_json[:can_update]).to be false
+ expect(as_json[:can_remove]).to be false
+ end
+ end
+ end
+
+ context 'when current user is not a group member' do
+ context 'when shared with group is public' do
+ it 'does expose shared_with_group details' do
+ expect(as_json[:shared_with_group].keys).to include(:id, :avatar_url, :web_url, :name)
+ end
+
+ it 'does expose source details' do
+ expect(as_json[:source].keys).to include(:id, :full_name)
+ end
+
+ it 'sets is_shared_with_group_private to false' do
+ expect(as_json[:is_shared_with_group_private]).to be false
+ end
+ end
+
+ context 'when shared with group is private' do
+ let_it_be(:shared_with_group) { create(:group, :private) }
+
+ let_it_be(:group_group_link) do
+ create(
+ :group_group_link,
+ {
+ shared_group: shared_group,
+ shared_with_group: shared_with_group,
+ expires_at: '2020-05-12'
+ }
+ )
+ end
+
+ it 'does not expose shared_with_group details' do
+ expect(as_json[:shared_with_group].keys).to contain_exactly(:id)
+ end
+
+ it 'does not expose source details' do
+ expect(as_json[:source]).to be_nil
+ end
- expect(json[:can_update]).to be false
- expect(json[:can_remove]).to be false
+ it 'sets is_shared_with_group_private to true' do
+ expect(as_json[:is_shared_with_group_private]).to be true
end
end
end
diff --git a/spec/serializers/group_link/project_group_link_entity_spec.rb b/spec/serializers/group_link/project_group_link_entity_spec.rb
index 1a8fcb2cfd3..00bfc43f17e 100644
--- a/spec/serializers/group_link/project_group_link_entity_spec.rb
+++ b/spec/serializers/group_link/project_group_link_entity_spec.rb
@@ -8,51 +8,69 @@ RSpec.describe GroupLink::ProjectGroupLinkEntity do
let(:entity) { described_class.new(project_group_link, { current_user: current_user, source: project_group_link.project }) }
- before do
- allow(entity).to receive(:current_user).and_return(current_user)
+ subject(:as_json) do
+ entity.as_json
end
it 'matches json schema' do
expect(entity.to_json).to match_schema('group_link/project_group_link')
end
- context 'when current user has `admin_project_member` permissions' do
- before do
- allow(entity).to receive(:can?).with(current_user, :admin_project_group_link, project_group_link).and_return(false)
- allow(entity).to receive(:can?).with(current_user, :admin_project_member, project_group_link.project).and_return(true)
+ context 'when current user is a project maintainer' do
+ before_all do
+ project_group_link.project.add_maintainer(current_user)
end
it 'exposes `can_update` and `can_remove` as `true`' do
- json = entity.as_json
-
- expect(json[:can_update]).to be true
- expect(json[:can_remove]).to be false
+ expect(as_json[:can_update]).to be true
+ expect(as_json[:can_remove]).to be true
end
end
context 'when current user is a group owner' do
- before do
- allow(entity).to receive(:can?).with(current_user, :admin_project_group_link, project_group_link).and_return(true)
- allow(entity).to receive(:can?).with(current_user, :admin_project_member, project_group_link.project).and_return(false)
+ before_all do
+ project_group_link.group.add_owner(current_user)
end
it 'exposes `can_remove` as true' do
- json = entity.as_json
-
- expect(json[:can_remove]).to be true
+ expect(as_json[:can_remove]).to be true
end
end
context 'when current user is not a group owner' do
- before do
- allow(entity).to receive(:can?).with(current_user, :admin_project_group_link, project_group_link).and_return(false)
- allow(entity).to receive(:can?).with(current_user, :admin_project_member, project_group_link.project).and_return(false)
+ it 'exposes `can_remove` as false' do
+ expect(as_json[:can_remove]).to be false
end
- it 'exposes `can_remove` as false' do
- json = entity.as_json
+ context 'when group is public' do
+ it 'does expose shared_with_group details' do
+ expect(as_json[:shared_with_group].keys).to include(:id, :avatar_url, :web_url, :name)
+ end
+
+ it 'does expose source details' do
+ expect(as_json[:source].keys).to include(:id, :full_name)
+ end
+
+ it 'sets is_shared_with_group_private to false' do
+ expect(as_json[:is_shared_with_group_private]).to be false
+ end
+ end
+
+ context 'when group is private' do
+ let_it_be(:private_group) { create(:group, :private) }
+ let_it_be(:project_group_link) { create(:project_group_link, group: private_group) }
+
+ it 'does not expose shared_with_group details' do
+ expect(as_json[:shared_with_group].keys).to contain_exactly(:id)
+ end
+
+ it 'does not expose source details' do
+ expect(as_json[:source]).to be_nil
+ end
- expect(json[:can_remove]).to be false
+ it 'sets is_shared_with_group_private to true' do
+ expect(as_json[:is_shared_with_group_private]).to be true
+ end
end
end
end
diff --git a/spec/serializers/issue_entity_spec.rb b/spec/serializers/issue_entity_spec.rb
index a8fd96a03bb..1faf4c6fe4c 100644
--- a/spec/serializers/issue_entity_spec.rb
+++ b/spec/serializers/issue_entity_spec.rb
@@ -149,7 +149,7 @@ RSpec.describe IssueEntity do
end
it 'returns archived project doc' do
- expect(subject[:archived_project_docs_path]).to eq('/help/user/project/settings/index.md#archive-a-project')
+ expect(subject[:archived_project_docs_path]).to eq('/help/user/project/settings/index#archive-a-project')
end
end
end
diff --git a/spec/serializers/merge_request_widget_entity_spec.rb b/spec/serializers/merge_request_widget_entity_spec.rb
index 8a0a2d38187..e9707265263 100644
--- a/spec/serializers/merge_request_widget_entity_spec.rb
+++ b/spec/serializers/merge_request_widget_entity_spec.rb
@@ -88,7 +88,7 @@ RSpec.describe MergeRequestWidgetEntity, feature_category: :code_review_workflow
let(:role) { :developer }
before do
- project.repository.create_file(user, Gitlab::FileDetector::PATTERNS[:gitlab_ci], 'CONTENT', message: 'Add .gitlab-ci.yml', branch_name: 'master')
+ project.repository.create_file(user, project.ci_config_path_or_default, 'CONTENT', message: 'Add .gitlab-ci.yml', branch_name: 'master')
end
it 'no ci config path' do
diff --git a/spec/serializers/review_app_setup_entity_spec.rb b/spec/serializers/review_app_setup_entity_spec.rb
index 9b068a2e9dd..9c6d54fd612 100644
--- a/spec/serializers/review_app_setup_entity_spec.rb
+++ b/spec/serializers/review_app_setup_entity_spec.rb
@@ -22,6 +22,10 @@ RSpec.describe ReviewAppSetupEntity do
expect(subject).to include(:can_setup_review_app)
end
+ it 'contains has_review_app' do
+ expect(subject).to include(:has_review_app)
+ end
+
context 'when the user can setup a review app' do
before do
allow(presenter).to receive(:can_setup_review_app?).and_return(true)
diff --git a/spec/services/activity_pub/accept_follow_service_spec.rb b/spec/services/activity_pub/accept_follow_service_spec.rb
new file mode 100644
index 00000000000..0f472412085
--- /dev/null
+++ b/spec/services/activity_pub/accept_follow_service_spec.rb
@@ -0,0 +1,77 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe ActivityPub::AcceptFollowService, feature_category: :integrations do
+ let_it_be(:project) { create(:project, :public) }
+ let_it_be_with_reload(:existing_subscription) do
+ create(:activity_pub_releases_subscription, :inbox, project: project)
+ end
+
+ let(:service) { described_class.new(existing_subscription, 'http://localhost/my-project/releases') }
+
+ describe '#execute' do
+ context 'when third party server complies' do
+ before do
+ allow(Gitlab::HTTP).to receive(:post).and_return(true)
+ service.execute
+ end
+
+ it 'sends an Accept activity' do
+ expect(Gitlab::HTTP).to have_received(:post)
+ end
+
+ it 'updates subscription state to accepted' do
+ expect(existing_subscription.reload.status).to eq 'accepted'
+ end
+ end
+
+ context 'when there is an error with third party server' do
+ before do
+ allow(Gitlab::HTTP).to receive(:post).and_raise(Errno::ECONNREFUSED)
+ end
+
+ it 'raises a ThirdPartyError' do
+ expect { service.execute }.to raise_error(ActivityPub::ThirdPartyError)
+ end
+
+ it 'does not update subscription state to accepted' do
+ begin
+ service.execute
+ rescue StandardError
+ end
+
+ expect(existing_subscription.reload.status).to eq 'requested'
+ end
+ end
+
+ context 'when subscription is already accepted' do
+ before do
+ allow(Gitlab::HTTP).to receive(:post).and_return(true)
+ allow(existing_subscription).to receive(:accepted!).and_return(true)
+ existing_subscription.status = :accepted
+ service.execute
+ end
+
+ it 'does not send an Accept activity' do
+ expect(Gitlab::HTTP).not_to have_received(:post)
+ end
+
+ it 'does not update subscription state' do
+ expect(existing_subscription).not_to have_received(:accepted!)
+ end
+ end
+
+ context 'when inbox has not been resolved' do
+ before do
+ allow(Gitlab::HTTP).to receive(:post).and_return(true)
+ allow(existing_subscription).to receive(:accepted!).and_return(true)
+ end
+
+ it 'raises an error' do
+ existing_subscription.subscriber_inbox_url = nil
+ expect { service.execute }.to raise_error(ActivityPub::AcceptFollowService::MissingInboxURLError)
+ end
+ end
+ end
+end
diff --git a/spec/services/activity_pub/inbox_resolver_service_spec.rb b/spec/services/activity_pub/inbox_resolver_service_spec.rb
new file mode 100644
index 00000000000..29048045bb5
--- /dev/null
+++ b/spec/services/activity_pub/inbox_resolver_service_spec.rb
@@ -0,0 +1,99 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe ActivityPub::InboxResolverService, feature_category: :integrations do
+ let_it_be(:project) { create(:project, :public) }
+ let_it_be_with_reload(:existing_subscription) { create(:activity_pub_releases_subscription, project: project) }
+ let(:service) { described_class.new(existing_subscription) }
+
+ shared_examples 'third party error' do
+ it 'raises a ThirdPartyError' do
+ expect { service.execute }.to raise_error(ActivityPub::ThirdPartyError)
+ end
+
+ it 'does not update the subscription record' do
+ begin
+ service.execute
+ rescue StandardError
+ end
+
+ expect(ActivityPub::ReleasesSubscription.last.subscriber_inbox_url).not_to eq 'https://example.com/user/inbox'
+ end
+ end
+
+ describe '#execute' do
+ context 'with successful HTTP request' do
+ before do
+ allow(Gitlab::HTTP).to receive(:get) { response }
+ end
+
+ let(:response) { instance_double(HTTParty::Response, body: body) }
+
+ context 'with a JSON response' do
+ let(:body) do
+ {
+ '@context': 'https://www.w3.org/ns/activitystreams',
+ id: 'https://example.com/user',
+ type: 'Person',
+ **inbox,
+ **entrypoints,
+ outbox: 'https://example.com/user/outbox'
+ }.to_json
+ end
+
+ let(:entrypoints) { {} }
+
+ context 'with valid response' do
+ let(:inbox) { { inbox: 'https://example.com/user/inbox' } }
+
+ context 'without a shared inbox' do
+ it 'updates only the inbox in the subscription record' do
+ service.execute
+
+ expect(ActivityPub::ReleasesSubscription.last.subscriber_inbox_url).to eq 'https://example.com/user/inbox'
+ expect(ActivityPub::ReleasesSubscription.last.shared_inbox_url).to be_nil
+ end
+ end
+
+ context 'with a shared inbox' do
+ let(:entrypoints) { { entrypoints: { sharedInbox: 'https://example.com/shared-inbox' } } }
+
+ it 'updates both the inbox and shared inbox in the subscription record' do
+ service.execute
+
+ expect(ActivityPub::ReleasesSubscription.last.subscriber_inbox_url).to eq 'https://example.com/user/inbox'
+ expect(ActivityPub::ReleasesSubscription.last.shared_inbox_url).to eq 'https://example.com/shared-inbox'
+ end
+ end
+ end
+
+ context 'without inbox attribute' do
+ let(:inbox) { {} }
+
+ it_behaves_like 'third party error'
+ end
+
+ context 'with a non string inbox attribute' do
+ let(:inbox) { { inbox: 27.13 } }
+
+ it_behaves_like 'third party error'
+ end
+ end
+
+ context 'with non JSON response' do
+ let(:body) { '<div>woops</div>' }
+
+ it_behaves_like 'third party error'
+ end
+ end
+
+ context 'with http error' do
+ before do
+ allow(Gitlab::HTTP).to receive(:get).and_raise(Errno::ECONNREFUSED)
+ end
+
+ it_behaves_like 'third party error'
+ end
+ end
+end
diff --git a/spec/services/admin/plan_limits/update_service_spec.rb b/spec/services/admin/plan_limits/update_service_spec.rb
index e57c234780c..eb9bbcf11aa 100644
--- a/spec/services/admin/plan_limits/update_service_spec.rb
+++ b/spec/services/admin/plan_limits/update_service_spec.rb
@@ -82,9 +82,9 @@ RSpec.describe Admin::PlanLimits::UpdateService, feature_category: :shared do
response = update_plan_limits
expect(response[:status]).to eq :error
- expect(response[:message]).to eq ["Notification limit must be greater than or equal to " \
- "storage_size_limit (Dashboard limit): 5 " \
- "and less than or equal to enforcement_limit: 10"]
+ expect(response[:message]).to eq [
+ "Notification limit must be greater than or equal to the dashboard limit (5)"
+ ]
end
end
@@ -102,9 +102,9 @@ RSpec.describe Admin::PlanLimits::UpdateService, feature_category: :shared do
response = update_plan_limits
expect(response[:status]).to eq :error
- expect(response[:message]).to eq ["Notification limit must be greater than or equal to " \
- "storage_size_limit (Dashboard limit): 5 " \
- "and less than or equal to enforcement_limit: 10"]
+ expect(response[:message]).to eq [
+ "Notification limit must be less than or equal to the enforcement limit (10)"
+ ]
end
end
@@ -113,8 +113,8 @@ RSpec.describe Admin::PlanLimits::UpdateService, feature_category: :shared do
before do
limits.update!(
- storage_size_limit: 12,
- notification_limit: 12
+ storage_size_limit: 10,
+ notification_limit: 9
)
end
@@ -122,9 +122,9 @@ RSpec.describe Admin::PlanLimits::UpdateService, feature_category: :shared do
response = update_plan_limits
expect(response[:status]).to eq :error
- expect(response[:message]).to eq ["Enforcement limit must be greater than " \
- "or equal to storage_size_limit (Dashboard limit): " \
- "12 and greater than or equal to notification_limit: 12"]
+ expect(response[:message]).to eq [
+ "Enforcement limit must be greater than or equal to the dashboard limit (10)"
+ ]
end
end
@@ -133,7 +133,7 @@ RSpec.describe Admin::PlanLimits::UpdateService, feature_category: :shared do
before do
limits.update!(
- storage_size_limit: 10,
+ storage_size_limit: 9,
notification_limit: 10
)
end
@@ -142,9 +142,9 @@ RSpec.describe Admin::PlanLimits::UpdateService, feature_category: :shared do
response = update_plan_limits
expect(response[:status]).to eq :error
- expect(response[:message]).to eq ["Enforcement limit must be greater than or equal to " \
- "storage_size_limit (Dashboard limit): " \
- "10 and greater than or equal to notification_limit: 10"]
+ expect(response[:message]).to eq [
+ "Enforcement limit must be greater than or equal to the notification limit (10)"
+ ]
end
end
@@ -162,8 +162,9 @@ RSpec.describe Admin::PlanLimits::UpdateService, feature_category: :shared do
response = update_plan_limits
expect(response[:status]).to eq :error
- expect(response[:message]).to eq ["Storage size limit (Dashboard limit) must be less than or " \
- "equal to enforcement_limit: 12 and notification_limit: 10"]
+ expect(response[:message]).to eq [
+ "Dashboard limit must be less than or equal to the notification limit (10)"
+ ]
end
end
@@ -181,8 +182,45 @@ RSpec.describe Admin::PlanLimits::UpdateService, feature_category: :shared do
response = update_plan_limits
expect(response[:status]).to eq :error
- expect(response[:message]).to eq ["Storage size limit (Dashboard limit) must be less than or " \
- "equal to enforcement_limit: 10 and notification_limit: 11"]
+ expect(response[:message]).to eq [
+ "Dashboard limit must be less than or equal to the enforcement limit (10)"
+ ]
+ end
+
+ context 'when enforcement_limit is 0' do
+ before do
+ limits.update!(
+ enforcement_limit: 0
+ )
+ end
+
+ it 'does not return an error' do
+ response = update_plan_limits
+
+ expect(response[:status]).to eq :success
+ end
+ end
+ end
+ end
+
+ context 'when setting limit to unlimited' do
+ before do
+ limits.update!(
+ notification_limit: 10,
+ storage_size_limit: 10,
+ enforcement_limit: 10
+ )
+ end
+
+ [:notification_limit, :enforcement_limit, :storage_size_limit].each do |limit|
+ context "for #{limit}" do
+ let(:params) { { limit => 0 } }
+
+ it 'is successful' do
+ response = update_plan_limits
+
+ expect(response[:status]).to eq :success
+ end
end
end
end
diff --git a/spec/services/application_settings/update_service_spec.rb b/spec/services/application_settings/update_service_spec.rb
index 0b5ba1db9d4..474d6ec4a9b 100644
--- a/spec/services/application_settings/update_service_spec.rb
+++ b/spec/services/application_settings/update_service_spec.rb
@@ -144,7 +144,7 @@ RSpec.describe ApplicationSettings::UpdateService, feature_category: :shared do
end
end
- describe 'performance bar settings', feature_category: :application_performance do
+ describe 'performance bar settings', feature_category: :cloud_connector do
using RSpec::Parameterized::TableSyntax
where(
@@ -321,7 +321,9 @@ RSpec.describe ApplicationSettings::UpdateService, feature_category: :shared do
let(:params) { { default_branch_protection: ::Gitlab::Access::PROTECTION_DEV_CAN_MERGE } }
it "updates default_branch_protection_defaults from the default_branch_protection param" do
- expect { subject.execute }.to change { application_settings.default_branch_protection_defaults }.from({}).to(expected)
+ default_value = ::Gitlab::Access::BranchProtection.protected_fully.deep_stringify_keys
+
+ expect { subject.execute }.to change { application_settings.default_branch_protection_defaults }.from(default_value).to(expected)
end
end
diff --git a/spec/services/auto_merge/base_service_spec.rb b/spec/services/auto_merge/base_service_spec.rb
index 8cd33f8ff1e..8329c4312cd 100644
--- a/spec/services/auto_merge/base_service_spec.rb
+++ b/spec/services/auto_merge/base_service_spec.rb
@@ -309,26 +309,18 @@ RSpec.describe AutoMerge::BaseService, feature_category: :code_review_workflow d
let(:merge_request) { create(:merge_request) }
- where(:can_be_merged, :open, :broken, :discussions, :blocked, :draft, :skip_draft, :skip_blocked,
- :skip_discussions, :result) do
- true | true | false | true | false | false | false | false | false | true
- true | true | false | true | false | false | true | true | false | true
- true | true | false | true | false | true | true | false | false | true
- true | true | false | true | true | false | false | true | false | true
- true | true | false | false | false | false | false | false | true | true
- true | true | false | true | false | true | false | false | false | false
- false | true | false | true | false | false | false | false | false | false
- true | false | false | true | false | false | false | false | false | false
- true | true | true | true | false | false | false | false | false | false
- true | true | false | false | false | false | false | false | false | false
- true | true | false | true | true | false | false | false | false | false
+ where(:can_be_merged, :open, :broken, :discussions, :blocked, :draft, :result) do
+ true | true | false | true | false | false | true
+ false | true | false | true | false | false | false
+ true | false | false | true | false | false | false
+ true | true | true | true | false | false | false
+ true | true | false | false | false | false | false
+ true | true | false | true | true | false | false
+ true | true | false | true | false | true | false
end
with_them do
before do
- allow(service).to receive(:skip_draft_check).and_return(skip_draft)
- allow(service).to receive(:skip_blocked_check).and_return(skip_blocked)
- allow(service).to receive(:skip_discussions_check).and_return(skip_discussions)
allow(merge_request).to receive(:can_be_merged_by?).and_return(can_be_merged)
allow(merge_request).to receive(:open?).and_return(open)
allow(merge_request).to receive(:broken?).and_return(broken)
diff --git a/spec/services/award_emojis/copy_service_spec.rb b/spec/services/award_emojis/copy_service_spec.rb
index 6c1d7fb21e2..81ec49d7741 100644
--- a/spec/services/award_emojis/copy_service_spec.rb
+++ b/spec/services/award_emojis/copy_service_spec.rb
@@ -25,7 +25,7 @@ RSpec.describe AwardEmojis::CopyService, feature_category: :team_planning do
it 'copies AwardEmojis', :aggregate_failures do
expect { execute_service }.to change { AwardEmoji.count }.by(2)
- expect(to_awardable.award_emoji.map(&:name)).to match_array(%w(thumbsup thumbsdown))
+ expect(to_awardable.award_emoji.map(&:name)).to match_array(%w[thumbsup thumbsdown])
end
it 'returns success', :aggregate_failures do
diff --git a/spec/services/bulk_imports/batched_relation_export_service_spec.rb b/spec/services/bulk_imports/batched_relation_export_service_spec.rb
index c361dfe5052..dd85961befd 100644
--- a/spec/services/bulk_imports/batched_relation_export_service_spec.rb
+++ b/spec/services/bulk_imports/batched_relation_export_service_spec.rb
@@ -71,29 +71,6 @@ RSpec.describe BulkImports::BatchedRelationExportService, feature_category: :imp
expect(export.batches.count).to eq(0)
end
end
-
- context 'when exception occurs' do
- it 'tracks exception and marks export as failed' do
- allow_next_instance_of(BulkImports::Export) do |export|
- allow(export).to receive(:update!).and_call_original
-
- allow(export)
- .to receive(:update!)
- .with(status_event: 'finish', total_objects_count: 0, batched: true, batches_count: 0, jid: jid, error: nil)
- .and_raise(StandardError, 'Error!')
- end
-
- expect(Gitlab::ErrorTracking)
- .to receive(:track_exception)
- .with(StandardError, portable_id: portable.id, portable_type: portable.class.name)
-
- service.execute
-
- export = portable.bulk_import_exports.first
-
- expect(export.reload.failed?).to eq(true)
- end
- end
end
describe '.cache_key' do
diff --git a/spec/services/bulk_imports/file_download_service_spec.rb b/spec/services/bulk_imports/file_download_service_spec.rb
index 1734ea45507..b2971c75bce 100644
--- a/spec/services/bulk_imports/file_download_service_spec.rb
+++ b/spec/services/bulk_imports/file_download_service_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
RSpec.describe BulkImports::FileDownloadService, feature_category: :importers do
describe '#execute' do
- let_it_be(:allowed_content_types) { %w(application/gzip application/octet-stream) }
+ let_it_be(:allowed_content_types) { %w[application/gzip application/octet-stream] }
let_it_be(:file_size_limit) { 5.gigabytes }
let_it_be(:config) { build(:bulk_import_configuration) }
let_it_be(:content_type) { 'application/octet-stream' }
@@ -82,18 +82,17 @@ RSpec.describe BulkImports::FileDownloadService, feature_category: :importers do
context 'when content-type is not valid' do
let(:content_type) { 'invalid' }
- let(:import_logger) { instance_double(Gitlab::Import::Logger) }
+ let(:import_logger) { instance_double(BulkImports::Logger) }
before do
- allow(Gitlab::Import::Logger).to receive(:build).and_return(import_logger)
+ allow(BulkImports::Logger).to receive(:build).and_return(import_logger)
allow(import_logger).to receive(:warn)
end
it 'logs and raises an error' do
expect(import_logger).to receive(:warn).once.with(
message: 'Invalid content type',
- response_headers: headers,
- importer: 'gitlab_migration'
+ response_headers: headers
)
expect { subject.execute }.to raise_error(described_class::ServiceError, 'Invalid content type')
diff --git a/spec/services/bulk_imports/lfs_objects_export_service_spec.rb b/spec/services/bulk_imports/lfs_objects_export_service_spec.rb
index 587c99d9897..b65862b30d2 100644
--- a/spec/services/bulk_imports/lfs_objects_export_service_spec.rb
+++ b/spec/services/bulk_imports/lfs_objects_export_service_spec.rb
@@ -15,7 +15,7 @@ RSpec.describe BulkImports::LfsObjectsExportService, feature_category: :importer
before do
stub_lfs_object_storage
- %w(wiki design).each do |repository_type|
+ %w[wiki design].each do |repository_type|
create(
:lfs_objects_project,
project: project,
diff --git a/spec/services/bulk_imports/process_service_spec.rb b/spec/services/bulk_imports/process_service_spec.rb
index 5398e76cb67..f5566819039 100644
--- a/spec/services/bulk_imports/process_service_spec.rb
+++ b/spec/services/bulk_imports/process_service_spec.rb
@@ -133,23 +133,6 @@ RSpec.describe BulkImports::ProcessService, feature_category: :importers do
end
end
end
-
- context 'when exception occurs' do
- it 'tracks the exception & marks import as failed' do
- create(:bulk_import_entity, :created, bulk_import: bulk_import)
-
- allow(BulkImports::ExportRequestWorker).to receive(:perform_async).and_raise(StandardError)
-
- expect(Gitlab::ErrorTracking).to receive(:track_exception).with(
- kind_of(StandardError),
- bulk_import_id: bulk_import.id
- )
-
- subject.execute
-
- expect(bulk_import.reload.failed?).to eq(true)
- end
- end
end
context 'when importing a group' do
@@ -221,15 +204,14 @@ RSpec.describe BulkImports::ProcessService, feature_category: :importers do
end
it 'logs an info message for the skipped pipelines' do
- expect_next_instance_of(Gitlab::Import::Logger) do |logger|
+ expect_next_instance_of(BulkImports::Logger) do |logger|
expect(logger).to receive(:info).with(
message: 'Pipeline skipped as source instance version not compatible with pipeline',
bulk_import_entity_id: entity.id,
bulk_import_id: entity.bulk_import_id,
bulk_import_entity_type: entity.source_type,
source_full_path: entity.source_full_path,
- importer: 'gitlab_migration',
- pipeline_name: 'PipelineClass4',
+ pipeline_class: 'PipelineClass4',
minimum_source_version: '15.1.0',
maximum_source_version: nil,
source_version: '15.0.0'
@@ -241,8 +223,7 @@ RSpec.describe BulkImports::ProcessService, feature_category: :importers do
bulk_import_id: entity.bulk_import_id,
bulk_import_entity_type: entity.source_type,
source_full_path: entity.source_full_path,
- importer: 'gitlab_migration',
- pipeline_name: 'PipelineClass5',
+ pipeline_class: 'PipelineClass5',
minimum_source_version: '16.0.0',
maximum_source_version: nil,
source_version: '15.0.0'
diff --git a/spec/services/bulk_imports/relation_batch_export_service_spec.rb b/spec/services/bulk_imports/relation_batch_export_service_spec.rb
index 8548e01a6aa..a18099a4189 100644
--- a/spec/services/bulk_imports/relation_batch_export_service_spec.rb
+++ b/spec/services/bulk_imports/relation_batch_export_service_spec.rb
@@ -10,7 +10,7 @@ RSpec.describe BulkImports::RelationBatchExportService, feature_category: :impor
let_it_be(:batch) { create(:bulk_import_export_batch, export: export) }
let_it_be(:cache_key) { BulkImports::BatchedRelationExportService.cache_key(export.id, batch.id) }
- subject(:service) { described_class.new(user.id, batch.id) }
+ subject(:service) { described_class.new(user, batch) }
before_all do
Gitlab::Cache::Import::Caching.set_add(cache_key, label.id)
@@ -34,10 +34,7 @@ RSpec.describe BulkImports::RelationBatchExportService, feature_category: :impor
end
it 'removes exported contents after export' do
- double = instance_double(BulkImports::FileTransfer::ProjectConfig, export_path: 'foo')
-
- allow(BulkImports::FileTransfer).to receive(:config_for).and_return(double)
- allow(double).to receive(:export_service_for).and_raise(StandardError, 'Error!')
+ allow(subject).to receive(:export_path).and_return('foo')
allow(FileUtils).to receive(:remove_entry)
expect(FileUtils).to receive(:remove_entry).with('foo')
@@ -53,29 +50,10 @@ RSpec.describe BulkImports::RelationBatchExportService, feature_category: :impor
allow(subject).to receive(:export_path).and_return('foo')
allow(FileUtils).to receive(:remove_entry)
- expect(FileUtils).to receive(:touch).with('foo/milestones.ndjson')
+ expect(FileUtils).to receive(:touch).with('foo/milestones.ndjson').and_call_original
subject.execute
end
end
-
- context 'when exception occurs' do
- before do
- allow(service).to receive(:gzip).and_raise(StandardError, 'Error!')
- end
-
- it 'marks batch as failed' do
- expect(Gitlab::ErrorTracking)
- .to receive(:track_exception)
- .with(StandardError, portable_id: project.id, portable_type: 'Project')
-
- service.execute
- batch.reload
-
- expect(batch.failed?).to eq(true)
- expect(batch.objects_count).to eq(0)
- expect(batch.error).to eq('Error!')
- end
- end
end
end
diff --git a/spec/services/bulk_imports/relation_export_service_spec.rb b/spec/services/bulk_imports/relation_export_service_spec.rb
index bd8447e3d97..b7d6c424277 100644
--- a/spec/services/bulk_imports/relation_export_service_spec.rb
+++ b/spec/services/bulk_imports/relation_export_service_spec.rb
@@ -59,7 +59,7 @@ RSpec.describe BulkImports::RelationExportService, feature_category: :importers
let(:relation) { 'milestones' }
it 'creates empty file on disk' do
- expect(FileUtils).to receive(:touch).with("#{export_path}/#{relation}.ndjson")
+ expect(FileUtils).to receive(:touch).with("#{export_path}/#{relation}.ndjson").and_call_original
subject.execute
end
@@ -118,39 +118,6 @@ RSpec.describe BulkImports::RelationExportService, feature_category: :importers
end
end
- context 'when exception occurs during export' do
- shared_examples 'tracks exception' do |exception_class|
- it 'tracks exception' do
- expect(Gitlab::ErrorTracking)
- .to receive(:track_exception)
- .with(exception_class, portable_id: group.id, portable_type: group.class.name)
- .and_call_original
-
- subject.execute
- end
- end
-
- before do
- allow_next_instance_of(BulkImports::ExportUpload) do |upload|
- allow(upload).to receive(:save!).and_raise(StandardError)
- end
- end
-
- it 'marks export as failed' do
- subject.execute
-
- expect(export.reload.failed?).to eq(true)
- end
-
- include_examples 'tracks exception', StandardError
-
- context 'when passed relation is not supported' do
- let(:relation) { 'unsupported' }
-
- include_examples 'tracks exception', ActiveRecord::RecordInvalid
- end
- end
-
context 'when export was batched' do
let(:relation) { 'milestones' }
let(:export) { create(:bulk_import_export, group: group, relation: relation, batched: true, batches_count: 2) }
diff --git a/spec/services/ci/cancel_pipeline_service_spec.rb b/spec/services/ci/cancel_pipeline_service_spec.rb
index c4a1e1c26d1..256d2db1ed2 100644
--- a/spec/services/ci/cancel_pipeline_service_spec.rb
+++ b/spec/services/ci/cancel_pipeline_service_spec.rb
@@ -12,12 +12,12 @@ RSpec.describe Ci::CancelPipelineService, :aggregate_failures, feature_category:
pipeline: pipeline,
current_user: current_user,
cascade_to_children: cascade_to_children,
- auto_canceled_by_pipeline_id: auto_canceled_by_pipeline_id,
+ auto_canceled_by_pipeline: auto_canceled_by_pipeline,
execute_async: execute_async)
end
let(:cascade_to_children) { true }
- let(:auto_canceled_by_pipeline_id) { nil }
+ let(:auto_canceled_by_pipeline) { nil }
let(:execute_async) { true }
shared_examples 'force_execute' do
@@ -58,14 +58,19 @@ RSpec.describe Ci::CancelPipelineService, :aggregate_failures, feature_category:
expect(pipeline.all_jobs.pluck(:status)).to match_array(%w[canceled canceled success])
end
- context 'when auto_canceled_by_pipeline_id is provided' do
- let(:auto_canceled_by_pipeline_id) { create(:ci_pipeline).id }
+ context 'when auto_canceled_by_pipeline is provided' do
+ let(:auto_canceled_by_pipeline) { create(:ci_pipeline) }
it 'updates the pipeline and jobs with it' do
subject
- expect(pipeline.auto_canceled_by_id).to eq(auto_canceled_by_pipeline_id)
- expect(pipeline.all_jobs.canceled.pluck(:auto_canceled_by_id).uniq).to eq([auto_canceled_by_pipeline_id])
+ expect(pipeline.auto_canceled_by_id).to eq(auto_canceled_by_pipeline.id)
+
+ expect(pipeline.all_jobs.canceled.pluck(:auto_canceled_by_id).uniq)
+ .to eq([auto_canceled_by_pipeline.id])
+
+ expect(pipeline.all_jobs.canceled.pluck(:auto_canceled_by_partition_id).uniq)
+ .to eq([auto_canceled_by_pipeline.partition_id])
end
end
diff --git a/spec/services/ci/catalog/resources/create_service_spec.rb b/spec/services/ci/catalog/resources/create_service_spec.rb
new file mode 100644
index 00000000000..202c76acaec
--- /dev/null
+++ b/spec/services/ci/catalog/resources/create_service_spec.rb
@@ -0,0 +1,49 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Ci::Catalog::Resources::CreateService, feature_category: :pipeline_composition do
+ let_it_be(:project) { create(:project, :catalog_resource_with_components) }
+ let_it_be(:user) { create(:user) }
+
+ let(:service) { described_class.new(project, user) }
+
+ before do
+ stub_licensed_features(ci_namespace_catalog: true)
+ end
+
+ describe '#execute' do
+ context 'with an unauthorized user' do
+ it 'raises an AccessDeniedError' do
+ expect { service.execute }.to raise_error(Gitlab::Access::AccessDeniedError)
+ end
+ end
+
+ context 'with an authorized user' do
+ before_all do
+ project.add_owner(user)
+ end
+
+ context 'and a valid project' do
+ it 'creates a catalog resource' do
+ response = service.execute
+
+ expect(response.payload.project).to eq(project)
+ end
+ end
+
+ context 'with an invalid catalog resource' do
+ it 'does not save the catalog resource' do
+ catalog_resource = instance_double(::Ci::Catalog::Resource,
+ valid?: false,
+ errors: instance_double(ActiveModel::Errors, full_messages: ['not valid']))
+ allow(::Ci::Catalog::Resource).to receive(:new).and_return(catalog_resource)
+
+ response = service.execute
+
+ expect(response.message).to eq('not valid')
+ end
+ end
+ end
+ end
+end
diff --git a/spec/services/ci/catalog/resources/release_service_spec.rb b/spec/services/ci/catalog/resources/release_service_spec.rb
new file mode 100644
index 00000000000..60cd6cb5f96
--- /dev/null
+++ b/spec/services/ci/catalog/resources/release_service_spec.rb
@@ -0,0 +1,62 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Ci::Catalog::Resources::ReleaseService, feature_category: :pipeline_composition do
+ describe '#execute' do
+ context 'with a valid catalog resource and release' do
+ it 'validates the catalog resource and creates a version' do
+ project = create(:project, :catalog_resource_with_components)
+ catalog_resource = create(:ci_catalog_resource, project: project)
+ release = create(:release, project: project, sha: project.repository.root_ref_sha)
+
+ response = described_class.new(release).execute
+
+ version = Ci::Catalog::Resources::Version.last
+
+ expect(response).to be_success
+ expect(version.release).to eq(release)
+ expect(version.catalog_resource).to eq(catalog_resource)
+ expect(version.catalog_resource.project).to eq(project)
+ end
+ end
+
+ context 'when the validation of the catalog resource fails' do
+ it 'returns an error and does not create a version' do
+ project = create(:project, :repository)
+ create(:ci_catalog_resource, project: project)
+ release = create(:release, project: project, sha: project.repository.root_ref_sha)
+
+ response = described_class.new(release).execute
+
+ expect(Ci::Catalog::Resources::Version.count).to be(0)
+ expect(response).to be_error
+ expect(response.message).to eq(
+ 'Project must have a description, ' \
+ 'Project must contain components. Ensure you are using the correct directory structure')
+ end
+ end
+
+ context 'when the creation of a version fails' do
+ it 'returns an error and does not create a version' do
+ project =
+ create(
+ :project, :custom_repo,
+ description: 'Component project',
+ files: {
+ 'templates/secret-detection.yml' => 'image: agent: coop',
+ 'README.md' => 'Read me'
+ }
+ )
+ create(:ci_catalog_resource, project: project)
+ release = create(:release, project: project, sha: project.repository.root_ref_sha)
+
+ response = described_class.new(release).execute
+
+ expect(Ci::Catalog::Resources::Version.count).to be(0)
+ expect(response).to be_error
+ expect(response.message).to include('mapping values are not allowed in this context')
+ end
+ end
+ end
+end
diff --git a/spec/services/ci/catalog/resources/validate_service_spec.rb b/spec/services/ci/catalog/resources/validate_service_spec.rb
index b43d98788e2..39ab758d78d 100644
--- a/spec/services/ci/catalog/resources/validate_service_spec.rb
+++ b/spec/services/ci/catalog/resources/validate_service_spec.rb
@@ -4,54 +4,85 @@ require 'spec_helper'
RSpec.describe Ci::Catalog::Resources::ValidateService, feature_category: :pipeline_composition do
describe '#execute' do
- context 'with a project that has a README and a description' do
+ context 'when a project has a README, a description, and at least one component' do
it 'is valid' do
- project = create(:project, :repository, description: 'Component project')
+ project = create(:project, :catalog_resource_with_components)
response = described_class.new(project, project.default_branch).execute
expect(response).to be_success
end
end
- context 'with a project that has neither a description nor a README' do
+ context 'when a project has neither a description nor a README nor components' do
it 'is not valid' do
- project = create(:project, :empty_repo)
- project.repository.create_file(
- project.creator,
- 'ruby.rb',
- 'I like this',
- message: 'Ruby like this',
- branch_name: 'master'
- )
+ project = create(:project, :small_repo)
response = described_class.new(project, project.default_branch).execute
- expect(response.message).to eq('Project must have a README , Project must have a description')
+ expect(response.message).to eq(
+ 'Project must have a README, ' \
+ 'Project must have a description, ' \
+ 'Project must contain components. Ensure you are using the correct directory structure')
end
end
- context 'with a project that has a description but not a README' do
+ context 'when a project has components but has neither a description nor a README' do
it 'is not valid' do
- project = create(:project, :empty_repo, description: 'project with no README')
- project.repository.create_file(
- project.creator,
- 'text.txt',
- 'I do not like this',
- message: 'only text like text',
- branch_name: 'master'
- )
+ project = create(:project, :small_repo, files: { 'templates/dast/template.yml' => 'image: alpine' })
response = described_class.new(project, project.default_branch).execute
- expect(response.message).to eq('Project must have a README')
+ expect(response.message).to eq('Project must have a README, Project must have a description')
+ end
+ end
+
+ context 'when a project has a description but has neither a README nor components' do
+ it 'is not valid' do
+ project = create(:project, :small_repo, description: 'project with no README and no components')
+ response = described_class.new(project, project.default_branch).execute
+
+ expect(response.message).to eq(
+ 'Project must have a README, ' \
+ 'Project must contain components. Ensure you are using the correct directory structure')
end
end
- context 'with a project that has a README and not a description' do
+ context 'when a project has a README but has neither a description nor components' do
it 'is not valid' do
project = create(:project, :repository)
response = described_class.new(project, project.default_branch).execute
+ expect(response.message).to eq(
+ 'Project must have a description, ' \
+ 'Project must contain components. Ensure you are using the correct directory structure')
+ end
+ end
+
+ context 'when a project has components and a description but no README' do
+ it 'is not valid' do
+ project = create(:project, :small_repo, description: 'desc', files: { 'templates/dast.yml' => 'image: alpine' })
+ response = described_class.new(project, project.default_branch).execute
+
+ expect(response.message).to eq('Project must have a README')
+ end
+ end
+
+ context 'when a project has components and a README but no description' do
+ it 'is not valid' do
+ project = create(:project, :custom_repo,
+ files: { 'templates/dast.yml' => 'image: alpine', 'README.md' => 'readme' })
+ response = described_class.new(project, project.default_branch).execute
+
expect(response.message).to eq('Project must have a description')
end
end
+
+ context 'when a project has a description and a README but no components' do
+ it 'is not valid' do
+ project = create(:project, :readme, description: 'project with no README and no components')
+ response = described_class.new(project, project.default_branch).execute
+
+ expect(response.message).to eq(
+ 'Project must contain components. Ensure you are using the correct directory structure')
+ end
+ end
end
end
diff --git a/spec/services/ci/catalog/resources/versions/create_service_spec.rb b/spec/services/ci/catalog/resources/versions/create_service_spec.rb
new file mode 100644
index 00000000000..e614a74a4a1
--- /dev/null
+++ b/spec/services/ci/catalog/resources/versions/create_service_spec.rb
@@ -0,0 +1,180 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Ci::Catalog::Resources::Versions::CreateService, feature_category: :pipeline_composition do
+ describe '#execute' do
+ let(:files) do
+ {
+ 'templates/secret-detection.yml' => "spec:\n inputs:\n website:\n---\nimage: alpine_1",
+ 'templates/dast/template.yml' => 'image: alpine_2',
+ 'templates/blank-yaml.yml' => '',
+ 'templates/dast/sub-folder/template.yml' => 'image: alpine_3',
+ 'templates/template.yml' => "spec:\n inputs:\n environment:\n---\nimage: alpine_6",
+ 'tests/test.yml' => 'image: alpine_7',
+ 'README.md' => 'Read me'
+ }
+ end
+
+ let(:project) do
+ create(
+ :project, :custom_repo,
+ description: 'Simple and Complex components',
+ files: files
+ )
+ end
+
+ let(:release) { create(:release, project: project, sha: project.repository.root_ref_sha) }
+ let!(:catalog_resource) { create(:ci_catalog_resource, project: project) }
+
+ context 'when the project is not a catalog resource' do
+ it 'does not create a version' do
+ project = create(:project, :repository)
+ release = create(:release, project: project, sha: project.repository.root_ref_sha)
+
+ response = described_class.new(release).execute
+
+ expect(response).to be_error
+ expect(response.message).to include('Project is not a catalog resource')
+ end
+ end
+
+ context 'when the catalog resource has different types of components and a release' do
+ it 'creates a version for the release' do
+ response = described_class.new(release).execute
+
+ expect(response).to be_success
+
+ version = Ci::Catalog::Resources::Version.last
+
+ expect(version.release).to eq(release)
+ expect(version.catalog_resource).to eq(catalog_resource)
+ expect(version.catalog_resource.project).to eq(project)
+ end
+
+ it 'marks the catalog resource as published' do
+ described_class.new(release).execute
+
+ expect(catalog_resource.reload.state).to eq('published')
+ end
+
+ context 'when the ci_catalog_create_metadata feature flag is disabled' do
+ before do
+ stub_feature_flags(ci_catalog_create_metadata: false)
+ end
+
+ it 'does not create components' do
+ expect(Ci::Catalog::Resources::Component).not_to receive(:bulk_insert!).and_call_original
+ expect(project.ci_components.count).to eq(0)
+
+ response = described_class.new(release).execute
+
+ expect(response).to be_success
+ expect(project.ci_components.count).to eq(0)
+ end
+ end
+
+ context 'when the ci_catalog_create_metadata feature flag is enabled' do
+ context 'when there are more than 10 components' do
+ let(:files) do
+ {
+ 'templates/secret11.yml' => '',
+ 'templates/secret10.yml' => '',
+ 'templates/secret8.yml' => '',
+ 'templates/secret7.yml' => '',
+ 'templates/secret6.yml' => '',
+ 'templates/secret5.yml' => '',
+ 'templates/secret4.yml' => '',
+ 'templates/secret3.yml' => '',
+ 'templates/secret2.yml' => '',
+ 'templates/secret1.yml' => '',
+ 'templates/secret0.yml' => '',
+ 'README.md' => 'Read me'
+ }
+ end
+
+ it 'does not create components' do
+ response = described_class.new(release).execute
+
+ expect(response).to be_error
+ expect(response.message).to include('Release cannot contain more than 10 components')
+ expect(project.ci_components.count).to eq(0)
+ end
+ end
+
+ it 'bulk inserts all the components' do
+ expect(Ci::Catalog::Resources::Component).to receive(:bulk_insert!).and_call_original
+
+ described_class.new(release).execute
+ end
+
+ it 'creates components for the catalog resource' do
+ expect(project.ci_components.count).to eq(0)
+ response = described_class.new(release).execute
+
+ expect(response).to be_success
+
+ version = Ci::Catalog::Resources::Version.last
+
+ expect(project.ci_components.count).to eq(4)
+ expect(project.ci_components.first.name).to eq('blank-yaml')
+ expect(project.ci_components.first.project).to eq(version.project)
+ expect(project.ci_components.first.inputs).to eq({})
+ expect(project.ci_components.first.catalog_resource).to eq(version.catalog_resource)
+ expect(project.ci_components.first.version).to eq(version)
+ expect(project.ci_components.first.path).to eq('templates/blank-yaml.yml')
+ expect(project.ci_components.second.name).to eq('dast')
+ expect(project.ci_components.second.project).to eq(version.project)
+ expect(project.ci_components.second.inputs).to eq({})
+ expect(project.ci_components.second.catalog_resource).to eq(version.catalog_resource)
+ expect(project.ci_components.second.version).to eq(version)
+ expect(project.ci_components.second.path).to eq('templates/dast/template.yml')
+ expect(project.ci_components.third.name).to eq('secret-detection')
+ expect(project.ci_components.third.project).to eq(version.project)
+ expect(project.ci_components.third.inputs).to eq({ "website" => nil })
+ expect(project.ci_components.third.catalog_resource).to eq(version.catalog_resource)
+ expect(project.ci_components.third.version).to eq(version)
+ expect(project.ci_components.third.path).to eq('templates/secret-detection.yml')
+ expect(project.ci_components.fourth.name).to eq('template')
+ expect(project.ci_components.fourth.project).to eq(version.project)
+ expect(project.ci_components.fourth.inputs).to eq({ "environment" => nil })
+ expect(project.ci_components.fourth.catalog_resource).to eq(version.catalog_resource)
+ expect(project.ci_components.fourth.version).to eq(version)
+ expect(project.ci_components.fourth.path).to eq('templates/template.yml')
+ end
+ end
+ end
+
+ context 'with invalid data' do
+ let_it_be(:files) do
+ {
+ 'templates/secret-detection.yml' => 'some: invalid: syntax',
+ 'README.md' => 'Read me'
+ }
+ end
+
+ it 'returns an error' do
+ response = described_class.new(release).execute
+
+ expect(response).to be_error
+ expect(response.message).to include('mapping values are not allowed in this context')
+ end
+ end
+
+ context 'when one or more components are invalid' do
+ let_it_be(:files) do
+ {
+ 'templates/secret-detection.yml' => "spec:\n inputs:\n - website\n---\nimage: alpine_1",
+ 'README.md' => 'Read me'
+ }
+ end
+
+ it 'returns an error' do
+ response = described_class.new(release).execute
+
+ expect(response).to be_error
+ expect(response.message).to include('Inputs must be a valid json schema')
+ end
+ end
+ end
+end
diff --git a/spec/services/ci/create_pipeline_service/pre_post_stages_spec.rb b/spec/services/ci/create_pipeline_service/pre_post_stages_spec.rb
index d935824e6cc..c0efb7cb639 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
@@ -39,9 +39,9 @@ RSpec.describe Ci::CreatePipelineService, :yaml_processor_feature_flag_corectnes
it 'creates a pipeline' do
expect(pipeline).to be_persisted
expect(pipeline.stages.map(&:name)).to contain_exactly(
- *%w(.pre build .post))
+ *%w[.pre build .post])
expect(pipeline.builds.map(&:name)).to contain_exactly(
- *%w(validate build notify))
+ *%w[validate build notify])
end
end
@@ -54,7 +54,7 @@ RSpec.describe Ci::CreatePipelineService, :yaml_processor_feature_flag_corectnes
# we can validate a list of stages, as they are assigned
# but not persisted
expect(pipeline.stages.map(&:name)).to contain_exactly(
- *%w(.pre .post))
+ *%w[.pre .post])
end
end
end
diff --git a/spec/services/ci/create_pipeline_service/rules_spec.rb b/spec/services/ci/create_pipeline_service/rules_spec.rb
index 05fa3cfeba3..fb448ab13dc 100644
--- a/spec/services/ci/create_pipeline_service/rules_spec.rb
+++ b/spec/services/ci/create_pipeline_service/rules_spec.rb
@@ -219,7 +219,7 @@ RSpec.describe Ci::CreatePipelineService, :yaml_processor_feature_flag_corectnes
let(:job1) { pipeline.builds.find_by(name: 'job1') }
let(:job2) { pipeline.builds.find_by(name: 'job2') }
- let(:variable_keys) { %w(VAR1 VAR2 VAR3 VAR4 VAR5 VAR6 VAR7) }
+ let(:variable_keys) { %w[VAR1 VAR2 VAR3 VAR4 VAR5 VAR6 VAR7] }
context 'when no match' do
let(:ref) { 'refs/heads/wip' }
diff --git a/spec/services/ci/create_pipeline_service_spec.rb b/spec/services/ci/create_pipeline_service_spec.rb
index 11f9708f9f3..19e55c22df8 100644
--- a/spec/services/ci/create_pipeline_service_spec.rb
+++ b/spec/services/ci/create_pipeline_service_spec.rb
@@ -1549,7 +1549,7 @@ RSpec.describe Ci::CreatePipelineService, :yaml_processor_feature_flag_corectnes
stage: 'build',
script: 'echo',
only: {
- variables: %w($CI)
+ variables: %w[$CI]
}
}
}
diff --git a/spec/services/ci/enqueue_job_service_spec.rb b/spec/services/ci/enqueue_job_service_spec.rb
index c2bb0bb2bb5..85983651148 100644
--- a/spec/services/ci/enqueue_job_service_spec.rb
+++ b/spec/services/ci/enqueue_job_service_spec.rb
@@ -78,4 +78,33 @@ RSpec.describe Ci::EnqueueJobService, '#execute', feature_category: :continuous_
execute
end
end
+
+ context 'when the job is manually triggered another user' do
+ let(:job_variables) do
+ [{ key: 'third', secret_value: 'third' },
+ { key: 'fourth', secret_value: 'fourth' }]
+ end
+
+ let(:service) do
+ described_class.new(build, current_user: user, variables: job_variables)
+ end
+
+ it 'assigns the user and variables to the job', :aggregate_failures do
+ called = false
+ service.execute do
+ unless called
+ called = true
+ raise ActiveRecord::StaleObjectError
+ end
+
+ build.enqueue!
+ end
+
+ build.reload
+
+ expect(called).to be true # ensure we actually entered the failure path
+ expect(build.user).to eq(user)
+ expect(build.job_variables.map(&:key)).to contain_exactly('third', 'fourth')
+ 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 88ccda90df0..6e263e82432 100644
--- a/spec/services/ci/pipeline_processing/atomic_processing_service_spec.rb
+++ b/spec/services/ci/pipeline_processing/atomic_processing_service_spec.rb
@@ -139,7 +139,7 @@ RSpec.describe Ci::PipelineProcessing::AtomicProcessingService, feature_category
fail_running_or_pending
- expect(builds_statuses).to eq %w(failed pending)
+ expect(builds_statuses).to eq %w[failed pending]
fail_running_or_pending
@@ -166,22 +166,22 @@ RSpec.describe Ci::PipelineProcessing::AtomicProcessingService, feature_category
succeed_running_or_pending
- expect(builds_names).to eq %w(build test)
- expect(builds_statuses).to eq %w(success pending)
+ expect(builds_names).to eq %w[build test]
+ expect(builds_statuses).to eq %w[success pending]
succeed_running_or_pending
- expect(builds_names).to eq %w(build test deploy production)
- expect(builds_statuses).to eq %w(success success pending manual)
+ expect(builds_names).to eq %w[build test deploy production]
+ expect(builds_statuses).to eq %w[success success pending manual]
succeed_running_or_pending
- expect(builds_names).to eq %w(build test deploy production cleanup clear:cache)
- expect(builds_statuses).to eq %w(success success success manual pending manual)
+ expect(builds_names).to eq %w[build test deploy production cleanup clear:cache]
+ expect(builds_statuses).to eq %w[success success success manual pending manual]
succeed_running_or_pending
- expect(builds_statuses).to eq %w(success success success manual success manual)
+ expect(builds_statuses).to eq %w[success success success manual success manual]
expect(pipeline.reload.status).to eq 'success'
end
end
@@ -194,22 +194,22 @@ RSpec.describe Ci::PipelineProcessing::AtomicProcessingService, feature_category
succeed_running_or_pending
- expect(builds_names).to eq %w(build test)
- expect(builds_statuses).to eq %w(success pending)
+ expect(builds_names).to eq %w[build test]
+ expect(builds_statuses).to eq %w[success pending]
fail_running_or_pending
- expect(builds_names).to eq %w(build test test_failure)
- expect(builds_statuses).to eq %w(success failed pending)
+ expect(builds_names).to eq %w[build test test_failure]
+ expect(builds_statuses).to eq %w[success failed pending]
succeed_running_or_pending
- expect(builds_names).to eq %w(build test test_failure cleanup)
- expect(builds_statuses).to eq %w(success failed success pending)
+ expect(builds_names).to eq %w[build test test_failure cleanup]
+ expect(builds_statuses).to eq %w[success failed success pending]
succeed_running_or_pending
- expect(builds_statuses).to eq %w(success failed success success)
+ expect(builds_statuses).to eq %w[success failed success success]
expect(pipeline.reload.status).to eq 'failed'
end
end
@@ -222,23 +222,23 @@ RSpec.describe Ci::PipelineProcessing::AtomicProcessingService, feature_category
succeed_running_or_pending
- expect(builds_names).to eq %w(build test)
- expect(builds_statuses).to eq %w(success pending)
+ expect(builds_names).to eq %w[build test]
+ expect(builds_statuses).to eq %w[success pending]
fail_running_or_pending
- expect(builds_names).to eq %w(build test test_failure)
- expect(builds_statuses).to eq %w(success failed pending)
+ expect(builds_names).to eq %w[build test test_failure]
+ expect(builds_statuses).to eq %w[success failed pending]
fail_running_or_pending
- expect(builds_names).to eq %w(build test test_failure cleanup)
- expect(builds_statuses).to eq %w(success failed failed pending)
+ expect(builds_names).to eq %w[build test test_failure cleanup]
+ expect(builds_statuses).to eq %w[success failed failed pending]
succeed_running_or_pending
- expect(builds_names).to eq %w(build test test_failure cleanup)
- expect(builds_statuses).to eq %w(success failed failed success)
+ expect(builds_names).to eq %w[build test test_failure cleanup]
+ expect(builds_statuses).to eq %w[success failed failed success]
expect(pipeline.reload.status).to eq('failed')
end
end
@@ -251,22 +251,22 @@ RSpec.describe Ci::PipelineProcessing::AtomicProcessingService, feature_category
succeed_running_or_pending
- expect(builds_names).to eq %w(build test)
- expect(builds_statuses).to eq %w(success pending)
+ expect(builds_names).to eq %w[build test]
+ expect(builds_statuses).to eq %w[success pending]
succeed_running_or_pending
- expect(builds_names).to eq %w(build test deploy production)
- expect(builds_statuses).to eq %w(success success pending manual)
+ expect(builds_names).to eq %w[build test deploy production]
+ expect(builds_statuses).to eq %w[success success pending manual]
fail_running_or_pending
- expect(builds_names).to eq %w(build test deploy production cleanup)
- expect(builds_statuses).to eq %w(success success failed manual pending)
+ expect(builds_names).to eq %w[build test deploy production cleanup]
+ expect(builds_statuses).to eq %w[success success failed manual pending]
succeed_running_or_pending
- expect(builds_statuses).to eq %w(success success failed manual success)
+ expect(builds_statuses).to eq %w[success success failed manual success]
expect(pipeline.reload).to be_failed
end
end
@@ -280,8 +280,8 @@ RSpec.describe Ci::PipelineProcessing::AtomicProcessingService, feature_category
succeed_running_or_pending
expect(builds.running_or_pending).not_to be_empty
- expect(builds_names).to eq %w(build test)
- expect(builds_statuses).to eq %w(success pending)
+ expect(builds_names).to eq %w[build test]
+ expect(builds_statuses).to eq %w[success pending]
cancel_running_or_pending
@@ -801,25 +801,25 @@ RSpec.describe Ci::PipelineProcessing::AtomicProcessingService, feature_category
it 'when linux:* finishes first it runs it out of order' do
expect(process_pipeline).to be_truthy
- expect(stages).to eq(%w(pending created created))
+ expect(stages).to eq(%w[pending created created])
expect(builds.pending).to contain_exactly(linux_build, mac_build)
# we follow the single path of linux
linux_build.reset.success!
- expect(stages).to eq(%w(running pending created))
+ expect(stages).to eq(%w[running pending created])
expect(builds.success).to contain_exactly(linux_build)
expect(builds.pending).to contain_exactly(mac_build, linux_rspec, linux_rubocop)
linux_rspec.reset.success!
- expect(stages).to eq(%w(running running created))
+ expect(stages).to eq(%w[running running created])
expect(builds.success).to contain_exactly(linux_build, linux_rspec)
expect(builds.pending).to contain_exactly(mac_build, linux_rubocop)
linux_rubocop.reset.success!
- expect(stages).to eq(%w(running running created))
+ expect(stages).to eq(%w[running running created])
expect(builds.success).to contain_exactly(linux_build, linux_rspec, linux_rubocop)
expect(builds.pending).to contain_exactly(mac_build)
@@ -827,7 +827,7 @@ RSpec.describe Ci::PipelineProcessing::AtomicProcessingService, feature_category
mac_rspec.reset.success!
mac_rubocop.reset.success!
- expect(stages).to eq(%w(success success pending))
+ expect(stages).to eq(%w[success success pending])
expect(builds.success).to contain_exactly(
linux_build, linux_rspec, linux_rubocop, mac_build, mac_rspec, mac_rubocop)
expect(builds.pending).to contain_exactly(deploy)
@@ -866,13 +866,13 @@ RSpec.describe Ci::PipelineProcessing::AtomicProcessingService, feature_category
it 'runs deploy_pages without waiting prior stages' do
expect(process_pipeline).to be_truthy
- expect(stages).to eq(%w(pending created pending))
+ expect(stages).to eq(%w[pending created pending])
expect(builds.pending).to contain_exactly(linux_build, mac_build, deploy_pages)
linux_build.reset.success!
deploy_pages.reset.success!
- expect(stages).to eq(%w(running pending running))
+ expect(stages).to eq(%w[running pending running])
expect(builds.success).to contain_exactly(linux_build, deploy_pages)
expect(builds.pending).to contain_exactly(mac_build, linux_rspec, linux_rubocop)
@@ -882,7 +882,7 @@ RSpec.describe Ci::PipelineProcessing::AtomicProcessingService, feature_category
mac_rspec.reset.success!
mac_rubocop.reset.success!
- expect(stages).to eq(%w(success success running))
+ expect(stages).to eq(%w[success success running])
expect(builds.pending).to contain_exactly(deploy)
end
end
@@ -900,12 +900,12 @@ RSpec.describe Ci::PipelineProcessing::AtomicProcessingService, feature_category
it 'skips the jobs depending on it' do
expect(process_pipeline).to be_truthy
- expect(stages).to eq(%w(pending created created))
+ expect(stages).to eq(%w[pending created created])
expect(all_builds.pending).to contain_exactly(linux_build)
linux_build.reset.drop!
- expect(stages).to eq(%w(failed skipped skipped))
+ expect(stages).to eq(%w[failed skipped skipped])
expect(all_builds.failed).to contain_exactly(linux_build)
expect(all_builds.skipped).to contain_exactly(linux_rspec, deploy)
end
@@ -922,7 +922,7 @@ RSpec.describe Ci::PipelineProcessing::AtomicProcessingService, feature_category
it 'makes deploy DAG to be skipped' do
expect(process_pipeline).to be_truthy
- expect(stages).to eq(%w(skipped skipped))
+ expect(stages).to eq(%w[skipped skipped])
expect(all_builds.manual).to contain_exactly(linux_build)
expect(all_builds.skipped).to contain_exactly(deploy)
end
@@ -1460,7 +1460,7 @@ RSpec.describe Ci::PipelineProcessing::AtomicProcessingService, feature_category
end
def delayed_options
- { when: 'delayed', options: { script: %w(echo), start_in: '1 minute' } }
+ { when: 'delayed', options: { script: %w[echo], start_in: '1 minute' } }
end
def unschedule
diff --git a/spec/services/ci/pipelines/update_metadata_service_spec.rb b/spec/services/ci/pipelines/update_metadata_service_spec.rb
new file mode 100644
index 00000000000..939ce7f5785
--- /dev/null
+++ b/spec/services/ci/pipelines/update_metadata_service_spec.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Ci::Pipelines::UpdateMetadataService, feature_category: :continuous_integration do
+ subject(:execute) { described_class.new(pipeline, { name: name }).execute }
+
+ let(:name) { 'Some random pipeline name' }
+
+ context 'when pipeline has no name' do
+ let(:pipeline) { create(:ci_pipeline) }
+
+ it 'updates the name' do
+ expect { execute }.to change { pipeline.reload.name }.to(name)
+ end
+ end
+
+ context 'when pipeline has a name' do
+ let(:pipeline) { create(:ci_pipeline, name: 'Some other name') }
+
+ it 'updates the name' do
+ expect { execute }.to change { pipeline.reload.name }.to(name)
+ end
+ end
+
+ context 'when new name is too long' do
+ let(:pipeline) { create(:ci_pipeline) }
+ let(:name) { 'a' * 256 }
+
+ it 'does not update the name' do
+ expect { execute }.not_to change { pipeline.reload.name }
+ end
+ end
+end
diff --git a/spec/services/ci/play_build_service_spec.rb b/spec/services/ci/play_build_service_spec.rb
index 46b6622d6ec..c5651dc4502 100644
--- a/spec/services/ci/play_build_service_spec.rb
+++ b/spec/services/ci/play_build_service_spec.rb
@@ -63,10 +63,6 @@ RSpec.describe Ci::PlayBuildService, '#execute', feature_category: :continuous_i
context 'when a subsequent job is skipped' do
let!(:job) { create(:ci_build, :skipped, pipeline: pipeline, stage_idx: build.stage_idx + 1) }
- before do
- create(:ci_build_need, build: job, name: build.name)
- end
-
it 'marks the subsequent job as processable' do
expect { service.execute(build) }.to change { job.reload.status }.from('skipped').to('created')
end
diff --git a/spec/services/ci/refs/enqueue_pipelines_to_unlock_service_spec.rb b/spec/services/ci/refs/enqueue_pipelines_to_unlock_service_spec.rb
index 468302cb689..052be3b2587 100644
--- a/spec/services/ci/refs/enqueue_pipelines_to_unlock_service_spec.rb
+++ b/spec/services/ci/refs/enqueue_pipelines_to_unlock_service_spec.rb
@@ -26,34 +26,40 @@ RSpec.describe Ci::Refs::EnqueuePipelinesToUnlockService, :unlock_pipelines, :cl
shared_examples_for 'unlocking pipelines' do
let(:is_tag) { target_ref.ref_path.include?(::Gitlab::Git::TAG_REF_PREFIX) }
- let!(:other_ref_pipeline) { create_pipeline(:locked, other_ref, tag: false) }
- let!(:old_unlocked_pipeline) { create_pipeline(:unlocked, ref) }
- let!(:older_locked_pipeline_1) { create_pipeline(:locked, ref) }
- let!(:older_locked_pipeline_2) { create_pipeline(:locked, ref) }
- let!(:older_locked_pipeline_3) { create_pipeline(:locked, ref) }
- let!(:older_child_pipeline) { create_pipeline(:locked, ref, child_of: older_locked_pipeline_3) }
- let!(:pipeline) { create_pipeline(:locked, ref) }
- let!(:child_pipeline) { create_pipeline(:locked, ref, child_of: pipeline) }
- let!(:newer_pipeline) { create_pipeline(:locked, ref) }
+ let!(:other_ref_pipeline) { create_pipeline(:locked, other_ref, :failed, tag: false) }
+ let!(:old_unlocked_pipeline) { create_pipeline(:unlocked, ref, :failed) }
+ let!(:old_locked_pipeline_1) { create_pipeline(:locked, ref, :failed) }
+ let!(:old_locked_pipeline_2) { create_pipeline(:locked, ref, :success) }
+ let!(:old_locked_pipeline_3) { create_pipeline(:locked, ref, :success) }
+ let!(:old_locked_pipeline_3_child) { create_pipeline(:locked, ref, :success, child_of: old_locked_pipeline_3) }
+ let!(:old_locked_pipeline_4) { create_pipeline(:locked, ref, :success) }
+ let!(:old_locked_pipeline_4_child) { create_pipeline(:locked, ref, :success, child_of: old_locked_pipeline_4) }
+ let!(:old_locked_pipeline_5) { create_pipeline(:locked, ref, :failed) }
+ let!(:old_locked_pipeline_5_child) { create_pipeline(:locked, ref, :success, child_of: old_locked_pipeline_5) }
+ let!(:pipeline) { create_pipeline(:locked, ref, :failed) }
+ let!(:child_pipeline) { create_pipeline(:locked, ref, :failed, child_of: pipeline) }
+ let!(:newer_pipeline) { create_pipeline(:locked, ref, :failed) }
context 'when before_pipeline is given' do
let(:before_pipeline) { pipeline }
- it 'only enqueues older locked pipelines within the ref' do
+ it 'only enqueues old locked pipelines within the ref, excluding the last successful CI source pipeline' do
expect { execute }
.to change { pipeline_ids_waiting_to_be_unlocked }
.from([])
.to([
- older_locked_pipeline_1.id,
- older_locked_pipeline_2.id,
- older_locked_pipeline_3.id,
- older_child_pipeline.id
+ old_locked_pipeline_1.id,
+ old_locked_pipeline_2.id,
+ old_locked_pipeline_3.id,
+ old_locked_pipeline_3_child.id,
+ old_locked_pipeline_5.id,
+ old_locked_pipeline_5_child.id
])
expect(execute).to include(
status: :success,
- total_pending_entries: 4,
- total_new_entries: 4
+ total_pending_entries: 6,
+ total_new_entries: 6
)
end
end
@@ -66,10 +72,14 @@ RSpec.describe Ci::Refs::EnqueuePipelinesToUnlockService, :unlock_pipelines, :cl
.to change { pipeline_ids_waiting_to_be_unlocked }
.from([])
.to([
- older_locked_pipeline_1.id,
- older_locked_pipeline_2.id,
- older_locked_pipeline_3.id,
- older_child_pipeline.id,
+ old_locked_pipeline_1.id,
+ old_locked_pipeline_2.id,
+ old_locked_pipeline_3.id,
+ old_locked_pipeline_3_child.id,
+ old_locked_pipeline_4.id,
+ old_locked_pipeline_4_child.id,
+ old_locked_pipeline_5.id,
+ old_locked_pipeline_5_child.id,
pipeline.id,
child_pipeline.id,
newer_pipeline.id
@@ -77,8 +87,8 @@ RSpec.describe Ci::Refs::EnqueuePipelinesToUnlockService, :unlock_pipelines, :cl
expect(execute).to include(
status: :success,
- total_pending_entries: 7,
- total_new_entries: 7
+ total_pending_entries: 11,
+ total_new_entries: 11
)
end
end
@@ -96,9 +106,9 @@ RSpec.describe Ci::Refs::EnqueuePipelinesToUnlockService, :unlock_pipelines, :cl
it_behaves_like 'unlocking pipelines'
end
- def create_pipeline(type, ref, tag: is_tag, child_of: nil)
+ def create_pipeline(type, ref, status, tag: is_tag, child_of: nil)
trait = type == :locked ? :artifacts_locked : :unlocked
- create(:ci_pipeline, trait, ref: ref, tag: tag, project: project, child_of: child_of).tap do |p|
+ create(:ci_pipeline, trait, status: status, ref: ref, tag: tag, project: project, child_of: child_of).tap do |p|
if child_of
build = create(:ci_build, pipeline: child_of)
create(:ci_sources_pipeline, source_job: build, source_project: project, pipeline: p, project: project)
diff --git a/spec/services/ci/register_job_service_spec.rb b/spec/services/ci/register_job_service_spec.rb
index 83bae16a30e..e38984281b0 100644
--- a/spec/services/ci/register_job_service_spec.rb
+++ b/spec/services/ci/register_job_service_spec.rb
@@ -948,7 +948,7 @@ module Ci
pending_job.create_queuing_entry!
end
- let(:runner) { create(:ci_runner, :instance, tag_list: %w(tag1 tag2)) }
+ let(:runner) { create(:ci_runner, :instance, tag_list: %w[tag1 tag2]) }
let(:expected_shared_runner) { true }
let(:expected_shard) { ::Gitlab::Ci::Queue::Metrics::DEFAULT_METRICS_SHARD }
let(:expected_jobs_running_for_project_first_job) { '0' }
@@ -957,14 +957,14 @@ module Ci
it_behaves_like 'metrics collector'
context 'when metrics_shard tag is defined' do
- let(:runner) { create(:ci_runner, :instance, tag_list: %w(tag1 metrics_shard::shard_tag tag2)) }
+ let(:runner) { create(:ci_runner, :instance, tag_list: %w[tag1 metrics_shard::shard_tag tag2]) }
let(:expected_shard) { 'shard_tag' }
it_behaves_like 'metrics collector'
end
context 'when multiple metrics_shard tag is defined' do
- let(:runner) { create(:ci_runner, :instance, tag_list: %w(tag1 metrics_shard::shard_tag metrics_shard::shard_tag_2 tag2)) }
+ let(:runner) { create(:ci_runner, :instance, tag_list: %w[tag1 metrics_shard::shard_tag metrics_shard::shard_tag_2 tag2]) }
let(:expected_shard) { 'shard_tag' }
it_behaves_like 'metrics collector'
@@ -997,7 +997,7 @@ module Ci
end
context 'when project runner is used' do
- let(:runner) { create(:ci_runner, :project, projects: [project], tag_list: %w(tag1 metrics_shard::shard_tag tag2)) }
+ let(:runner) { create(:ci_runner, :project, projects: [project], tag_list: %w[tag1 metrics_shard::shard_tag tag2]) }
let(:expected_shared_runner) { false }
let(:expected_shard) { ::Gitlab::Ci::Queue::Metrics::DEFAULT_METRICS_SHARD }
let(:expected_jobs_running_for_project_first_job) { '+Inf' }
diff --git a/spec/services/ci/retry_job_service_spec.rb b/spec/services/ci/retry_job_service_spec.rb
index 80fbfc04f9b..1646afde21d 100644
--- a/spec/services/ci/retry_job_service_spec.rb
+++ b/spec/services/ci/retry_job_service_spec.rb
@@ -270,14 +270,6 @@ RSpec.describe Ci::RetryJobService, feature_category: :continuous_integration do
it_behaves_like 'creates associations for a deployable job', :ci_bridge
end
- context 'when `create_deployment_only_for_processable_jobs` FF is disabled' do
- before do
- stub_feature_flags(create_deployment_only_for_processable_jobs: false)
- end
-
- it_behaves_like 'creates associations for a deployable job', :ci_bridge
- end
-
context 'when given variables' do
let(:new_job) { service.clone!(job, variables: job_variables_attributes) }
@@ -302,14 +294,6 @@ RSpec.describe Ci::RetryJobService, feature_category: :continuous_integration do
it_behaves_like 'creates associations for a deployable job', :ci_build
end
- context 'when `create_deployment_only_for_processable_jobs` FF is disabled' do
- before do
- stub_feature_flags(create_deployment_only_for_processable_jobs: false)
- end
-
- it_behaves_like 'creates associations for a deployable job', :ci_build
- end
-
context 'when given variables' do
let(:new_job) { service.clone!(job, variables: job_variables_attributes) }
diff --git a/spec/services/ci/retry_pipeline_service_spec.rb b/spec/services/ci/retry_pipeline_service_spec.rb
index 6d991baafd0..125dbc5083c 100644
--- a/spec/services/ci/retry_pipeline_service_spec.rb
+++ b/spec/services/ci/retry_pipeline_service_spec.rb
@@ -122,7 +122,7 @@ RSpec.describe Ci::RetryPipelineService, '#execute', feature_category: :continuo
expect(build('build')).to be_success
expect(build('build2')).to be_success
expect(build('test')).to be_pending
- expect(build('test').needs.map(&:name)).to match_array(%w(build build2))
+ expect(build('test').needs.map(&:name)).to match_array(%w[build build2])
end
context 'when there is a failed DAG test without needs' do
diff --git a/spec/services/ci/runners/register_runner_service_spec.rb b/spec/services/ci/runners/register_runner_service_spec.rb
index b5921773364..4b997855657 100644
--- a/spec/services/ci/runners/register_runner_service_spec.rb
+++ b/spec/services/ci/runners/register_runner_service_spec.rb
@@ -74,7 +74,7 @@ RSpec.describe ::Ci::Runners::RegisterRunnerService, '#execute', feature_categor
active: false,
locked: true,
run_untagged: false,
- tag_list: %w(tag1 tag2),
+ tag_list: %w[tag1 tag2],
access_level: 'ref_protected',
maximum_timeout: 600,
name: 'some name',
@@ -290,7 +290,7 @@ RSpec.describe ::Ci::Runners::RegisterRunnerService, '#execute', feature_categor
let(:token) { registration_token }
let(:args) do
- { tag_list: %w(tag1 tag2) }
+ { tag_list: %w[tag1 tag2] }
end
it 'creates runner with tags' do
diff --git a/spec/services/ci/stuck_builds/drop_pending_service_spec.rb b/spec/services/ci/stuck_builds/drop_pending_service_spec.rb
index 6d91f5098eb..9da63930057 100644
--- a/spec/services/ci/stuck_builds/drop_pending_service_spec.rb
+++ b/spec/services/ci/stuck_builds/drop_pending_service_spec.rb
@@ -139,7 +139,7 @@ RSpec.describe Ci::StuckBuilds::DropPendingService, feature_category: :runner_fl
end
end
- %w(success skipped failed canceled).each do |status|
+ %w[success skipped failed canceled].each do |status|
context "when job is #{status}" do
let(:status) { status }
let(:updated_at) { 2.days.ago }
diff --git a/spec/services/ci/stuck_builds/drop_running_service_spec.rb b/spec/services/ci/stuck_builds/drop_running_service_spec.rb
index deb807753c2..c2f8a643f24 100644
--- a/spec/services/ci/stuck_builds/drop_running_service_spec.rb
+++ b/spec/services/ci/stuck_builds/drop_running_service_spec.rb
@@ -51,7 +51,7 @@ RSpec.describe Ci::StuckBuilds::DropRunningService, feature_category: :runner_fl
include_examples 'running builds'
end
- %w(success skipped failed canceled scheduled pending).each do |status|
+ %w[success skipped failed canceled scheduled pending].each do |status|
context "when job is #{status}" do
let(:status) { status }
let(:updated_at) { 2.days.ago }
diff --git a/spec/services/ci/stuck_builds/drop_scheduled_service_spec.rb b/spec/services/ci/stuck_builds/drop_scheduled_service_spec.rb
index f2e658c3ae3..5560eaf9b40 100644
--- a/spec/services/ci/stuck_builds/drop_scheduled_service_spec.rb
+++ b/spec/services/ci/stuck_builds/drop_scheduled_service_spec.rb
@@ -23,7 +23,7 @@ RSpec.describe Ci::StuckBuilds::DropScheduledService, feature_category: :runner_
end
end
- %w(success skipped failed canceled running pending).each do |status|
+ %w[success skipped failed canceled running pending].each do |status|
context "when job is #{status}" do
before do
job.update!(status: status)
diff --git a/spec/services/clusters/kubernetes/create_or_update_service_account_service_spec.rb b/spec/services/clusters/kubernetes/create_or_update_service_account_service_spec.rb
index e8e0174fe40..b5cf45e7b36 100644
--- a/spec/services/clusters/kubernetes/create_or_update_service_account_service_spec.rb
+++ b/spec/services/clusters/kubernetes/create_or_update_service_account_service_spec.rb
@@ -221,9 +221,9 @@ RSpec.describe Clusters::Kubernetes::CreateOrUpdateServiceAccountService, featur
namespace: namespace
},
rules: [{
- apiGroups: %w(serving.knative.dev),
- resources: %w(configurations configurationgenerations routes revisions revisionuids autoscalers services),
- verbs: %w(get list create update delete patch watch)
+ apiGroups: %w[serving.knative.dev],
+ resources: %w[configurations configurationgenerations routes revisions revisionuids autoscalers services],
+ verbs: %w[get list create update delete patch watch]
}]
)
)
@@ -239,9 +239,9 @@ RSpec.describe Clusters::Kubernetes::CreateOrUpdateServiceAccountService, featur
namespace: namespace
},
rules: [{
- apiGroups: %w(database.crossplane.io),
- resources: %w(postgresqlinstances),
- verbs: %w(get list create watch)
+ apiGroups: %w[database.crossplane.io],
+ resources: %w[postgresqlinstances],
+ verbs: %w[get list create watch]
}]
)
)
diff --git a/spec/services/container_registry/protection/create_rule_service_spec.rb b/spec/services/container_registry/protection/create_rule_service_spec.rb
new file mode 100644
index 00000000000..3c319caf25c
--- /dev/null
+++ b/spec/services/container_registry/protection/create_rule_service_spec.rb
@@ -0,0 +1,145 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe ContainerRegistry::Protection::CreateRuleService, '#execute', feature_category: :container_registry do
+ let_it_be(:project) { create(:project, :repository) }
+ let_it_be(:current_user) { create(:user, maintainer_projects: [project]) }
+
+ let(:service) { described_class.new(project, current_user, params) }
+ let(:params) { attributes_for(:container_registry_protection_rule) }
+
+ subject { service.execute }
+
+ shared_examples 'a successful service response' do
+ it { is_expected.to be_success }
+
+ it { is_expected.to have_attributes(errors: be_blank) }
+
+ it do
+ is_expected.to have_attributes(
+ payload: {
+ container_registry_protection_rule:
+ be_a(ContainerRegistry::Protection::Rule)
+ .and(have_attributes(
+ container_path_pattern: params[:container_path_pattern],
+ push_protected_up_to_access_level: params[:push_protected_up_to_access_level].to_s,
+ delete_protected_up_to_access_level: params[:delete_protected_up_to_access_level].to_s
+ ))
+ }
+ )
+ end
+
+ it 'creates a new container registry protection rule in the database' do
+ expect { subject }.to change { ContainerRegistry::Protection::Rule.count }.by(1)
+
+ expect(
+ ContainerRegistry::Protection::Rule.where(
+ project: project,
+ container_path_pattern: params[:container_path_pattern],
+ push_protected_up_to_access_level: params[:push_protected_up_to_access_level]
+ )
+ ).to exist
+ end
+ end
+
+ shared_examples 'an erroneous service response' do
+ it { is_expected.to be_error }
+ it { is_expected.to have_attributes(errors: be_present, payload: include(container_registry_protection_rule: nil)) }
+
+ it 'does not create a new container registry protection rule in the database' do
+ expect { subject }.not_to change { ContainerRegistry::Protection::Rule.count }
+ end
+
+ it 'does not create a container registry protection rule with the given params' do
+ subject
+
+ expect(
+ ContainerRegistry::Protection::Rule.where(
+ project: project,
+ container_path_pattern: params[:container_path_pattern],
+ push_protected_up_to_access_level: params[:push_protected_up_to_access_level]
+ )
+ ).not_to exist
+ end
+ end
+
+ it_behaves_like 'a successful service response'
+
+ context 'when fields are invalid' do
+ context 'when container_path_pattern is invalid' do
+ let(:params) { super().merge(container_path_pattern: '') }
+
+ it_behaves_like 'an erroneous service response'
+
+ it { is_expected.to have_attributes(message: match(/Container path pattern can't be blank/)) }
+ end
+
+ context 'when delete_protected_up_to_access_level is invalid' do
+ let(:params) { super().merge(delete_protected_up_to_access_level: 1000) }
+
+ it_behaves_like 'an erroneous service response'
+
+ it { is_expected.to have_attributes(message: match(/is not a valid delete_protected_up_to_access_level/)) }
+ end
+
+ context 'when push_protected_up_to_access_level is invalid' do
+ let(:params) { super().merge(push_protected_up_to_access_level: 1000) }
+
+ it_behaves_like 'an erroneous service response'
+
+ it { is_expected.to have_attributes(message: match(/is not a valid push_protected_up_to_access_level/)) }
+ end
+ end
+
+ context 'with existing container registry protection rule in the database' do
+ let_it_be_with_reload(:existing_container_registry_protection_rule) do
+ create(:container_registry_protection_rule, project: project)
+ end
+
+ context 'when container registry name pattern is slightly different' do
+ let(:params) do
+ super().merge(
+ # The field `container_path_pattern` is unique; this is why we change the value in a minimum way
+ container_path_pattern: "#{existing_container_registry_protection_rule.container_path_pattern}-unique",
+ push_protected_up_to_access_level:
+ existing_container_registry_protection_rule.push_protected_up_to_access_level
+ )
+ end
+
+ it_behaves_like 'a successful service response'
+ end
+
+ context 'when field `container_path_pattern` is taken' do
+ let(:params) do
+ super().merge(
+ container_path_pattern: existing_container_registry_protection_rule.container_path_pattern,
+ push_protected_up_to_access_level: :maintainer
+ )
+ end
+
+ it_behaves_like 'an erroneous service response'
+
+ it { is_expected.to have_attributes(errors: ['Container path pattern has already been taken']) }
+
+ it { expect { subject }.not_to change { existing_container_registry_protection_rule.updated_at } }
+ end
+ end
+
+ context 'with disallowed params' do
+ let(:params) { super().merge(project_id: 1, unsupported_param: 'unsupported_param_value') }
+
+ it_behaves_like 'a successful service response'
+ end
+
+ context 'with forbidden user access level (project developer role)' do
+ # Because of the access level hierarchy, we can assume that
+ # other access levels below developer role will also not be able to
+ # create container registry protection rules.
+ let_it_be(:current_user) { create(:user).tap { |u| project.add_developer(u) } }
+
+ it_behaves_like 'an erroneous service response'
+
+ it { is_expected.to have_attributes(message: match(/Unauthorized/)) }
+ end
+end
diff --git a/spec/services/deployments/update_environment_service_spec.rb b/spec/services/deployments/update_environment_service_spec.rb
index 79bf0d972d4..bc6e244dc2f 100644
--- a/spec/services/deployments/update_environment_service_spec.rb
+++ b/spec/services/deployments/update_environment_service_spec.rb
@@ -145,7 +145,7 @@ RSpec.describe Deployments::UpdateEnvironmentService, feature_category: :continu
an_instance_of(described_class::EnvironmentUpdateFailure),
project_id: project.id,
environment_id: environment.id,
- reason: %q{External url javascript scheme is not allowed}
+ reason: %q(External url javascript scheme is not allowed)
)
.once
diff --git a/spec/services/design_management/copy_design_collection/copy_service_spec.rb b/spec/services/design_management/copy_design_collection/copy_service_spec.rb
index 048327792e0..2f858e86cf1 100644
--- a/spec/services/design_management/copy_design_collection/copy_service_spec.rb
+++ b/spec/services/design_management/copy_design_collection/copy_service_spec.rb
@@ -267,7 +267,7 @@ RSpec.describe DesignManagement::CopyDesignCollection::CopyService, :clean_gitla
let_it_be(:config_file) { Rails.root.join('lib/gitlab/design_management/copy_design_collection_model_attributes.yml') }
let_it_be(:config) { YAML.load_file(config_file).symbolize_keys }
- %w(Design Action Version).each do |model|
+ %w[Design Action Version].each do |model|
specify do
attributes = config["#{model.downcase}_attributes".to_sym] || []
ignored_attributes = config["ignore_#{model.downcase}_attributes".to_sym]
diff --git a/spec/services/draft_notes/publish_service_spec.rb b/spec/services/draft_notes/publish_service_spec.rb
index e087f2ffc7e..fbc38f93c56 100644
--- a/spec/services/draft_notes/publish_service_spec.rb
+++ b/spec/services/draft_notes/publish_service_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
RSpec.describe DraftNotes::PublishService, feature_category: :code_review_workflow do
include RepoHelpers
- let(:merge_request) { create(:merge_request) }
+ let_it_be(:merge_request) { create(:merge_request, reviewers: create_list(:user, 1)) }
let(:project) { merge_request.target_project }
let(:user) { merge_request.author }
let(:commit) { project.commit(sample_commit.id) }
@@ -198,6 +198,29 @@ RSpec.describe DraftNotes::PublishService, feature_category: :code_review_workfl
end
end
end
+
+ it 'does not call UpdateReviewerStateService' do
+ publish
+
+ expect(MergeRequests::UpdateReviewerStateService).not_to receive(:new)
+ end
+
+ context 'when `mr_request_changes` feature flag is disabled' do
+ before do
+ stub_feature_flags(mr_request_changes: false)
+ end
+
+ it 'calls UpdateReviewerStateService' do
+ expect_next_instance_of(
+ MergeRequests::UpdateReviewerStateService,
+ project: project, current_user: user
+ ) do |service|
+ expect(service).to receive(:execute).with(merge_request, "reviewed")
+ end
+
+ publish
+ end
+ end
end
context 'draft notes with suggestions' do
diff --git a/spec/services/environments/auto_recover_service_spec.rb b/spec/services/environments/auto_recover_service_spec.rb
new file mode 100644
index 00000000000..9807e8f9314
--- /dev/null
+++ b/spec/services/environments/auto_recover_service_spec.rb
@@ -0,0 +1,99 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Environments::AutoRecoverService, :clean_gitlab_redis_shared_state, :sidekiq_inline,
+ feature_category: :continuous_delivery do
+ include CreateEnvironmentsHelpers
+ include ExclusiveLeaseHelpers
+
+ let_it_be(:project) { create(:project, :repository) }
+ let_it_be(:user) { create(:user) }
+
+ let(:service) { described_class.new }
+
+ before_all do
+ project.add_developer(user)
+ end
+
+ describe '#execute' do
+ subject { service.execute }
+
+ let_it_be(:project) { create(:project, :repository) }
+ let_it_be(:user) { create(:user) }
+
+ let(:environments) { Environment.all }
+
+ before_all do
+ project.add_developer(user)
+ project.repository.add_branch(user, 'review/feature-1', 'master')
+ project.repository.add_branch(user, 'review/feature-2', 'master')
+ end
+
+ before do
+ create_review_app(user, project, 'review/feature-1')
+ create_review_app(user, project, 'review/feature-2')
+
+ Environment.all.map do |e|
+ e.stop_actions.map(&:drop)
+ e.stop!
+ e.update!(updated_at: (Environment::LONG_STOP + 1.day).ago)
+ e.reload
+ end
+ end
+
+ it 'stops environments that have been stuck stopping too long' do
+ expect { subject }
+ .to change { Environment.all.map(&:state).uniq }
+ .from(['stopping']).to(['available'])
+ end
+
+ it 'schedules stop processes in bulk' do
+ args = [[Environment.find_by_name('review/feature-1').id], [Environment.find_by_name('review/feature-2').id]]
+
+ expect(Environments::AutoRecoverWorker)
+ .to receive(:bulk_perform_async).with(args).once.and_call_original
+
+ subject
+ end
+
+ context 'when the other sidekiq worker has already been running' do
+ before do
+ stub_exclusive_lease_taken(described_class::EXCLUSIVE_LOCK_KEY)
+ end
+
+ it 'does not execute recover_in_batch' do
+ expect_next_instance_of(described_class) do |service|
+ expect(service).not_to receive(:recover_in_batch)
+ end
+
+ expect { subject }.to raise_error(Gitlab::ExclusiveLeaseHelpers::FailedToObtainLockError)
+ end
+ end
+
+ context 'when loop reached timeout' do
+ before do
+ stub_const("#{described_class}::LOOP_TIMEOUT", 0.seconds)
+ stub_const("#{described_class}::LOOP_LIMIT", 100_000)
+ allow_next_instance_of(described_class) do |service|
+ allow(service).to receive(:recover_in_batch).and_return(true)
+ end
+ end
+
+ it 'returns false and does not continue the process' do
+ is_expected.to eq(false)
+ end
+ end
+
+ context 'when loop reached loop limit' do
+ before do
+ stub_const("#{described_class}::LOOP_LIMIT", 1)
+ stub_const("#{described_class}::BATCH_SIZE", 1)
+ end
+
+ it 'stops only one available environment' do
+ expect { subject }.to change { Environment.long_stopping.count }.by(-1)
+ end
+ end
+ end
+end
diff --git a/spec/services/git/branch_hooks_service_spec.rb b/spec/services/git/branch_hooks_service_spec.rb
index 3050d6c5eca..8fd542542ae 100644
--- a/spec/services/git/branch_hooks_service_spec.rb
+++ b/spec/services/git/branch_hooks_service_spec.rb
@@ -133,27 +133,14 @@ RSpec.describe Git::BranchHooksService, :clean_gitlab_redis_shared_state, featur
expect(Gitlab::UsageDataCounters::HLLRedisCounter.unique_events(event_names: 'o_pipeline_authoring_unique_users_committing_ciconfigfile', start_date: time, end_date: time + 7.days)).to eq(1)
end
- context 'when usage ping is disabled' do
- before do
- allow(::ServicePing::ServicePingSettings).to receive(:enabled?).and_return(false)
- end
-
- it 'does not track the event' do
- execute_service
-
- expect(Gitlab::UsageDataCounters::HLLRedisCounter)
- .not_to receive(:track_event).with(*tracking_params)
- end
- end
-
context 'when the branch is not the main branch' do
let(:branch) { 'feature' }
it 'does not track the event' do
- execute_service
-
expect(Gitlab::UsageDataCounters::HLLRedisCounter)
.not_to receive(:track_event).with(*tracking_params)
+
+ execute_service
end
end
@@ -163,10 +150,10 @@ RSpec.describe Git::BranchHooksService, :clean_gitlab_redis_shared_state, featur
end
it 'does not track the event' do
- execute_service
-
expect(Gitlab::UsageDataCounters::HLLRedisCounter)
.not_to receive(:track_event).with(*tracking_params)
+
+ execute_service
end
end
end
diff --git a/spec/services/git/branch_push_service_spec.rb b/spec/services/git/branch_push_service_spec.rb
index fe54663b983..db4f3ace64b 100644
--- a/spec/services/git/branch_push_service_spec.rb
+++ b/spec/services/git/branch_push_service_spec.rb
@@ -125,7 +125,7 @@ RSpec.describe Git::BranchPushService, :use_clean_rails_redis_caching, services:
allow(Gitlab::Runtime).to receive(:sidekiq?).and_return(true)
expect(Sidekiq.logger).to receive(:warn) do |args|
pipeline_params = args[:pipeline_params]
- expect(pipeline_params.keys).to match_array(%i(before after ref variables_attributes checkout_sha))
+ expect(pipeline_params.keys).to match_array(%i[before after ref variables_attributes checkout_sha])
end
expect { subject }.not_to change { Ci::Pipeline.count }
diff --git a/spec/services/git/process_ref_changes_service_spec.rb b/spec/services/git/process_ref_changes_service_spec.rb
index 9ec13bc957b..93d65b0b344 100644
--- a/spec/services/git/process_ref_changes_service_spec.rb
+++ b/spec/services/git/process_ref_changes_service_spec.rb
@@ -236,7 +236,7 @@ RSpec.describe Git::ProcessRefChangesService, feature_category: :source_code_man
before do
allow(MergeRequests::PushedBranchesService).to receive(:new).and_return(
- double(execute: %w(create1 create2)), double(execute: %w(changed1)), double(execute: %w(removed2))
+ double(execute: %w[create1 create2]), double(execute: %w[changed1]), double(execute: %w[removed2])
)
allow(Gitlab::Git::Commit).to receive(:between).and_return([])
diff --git a/spec/services/google_cloud/generate_pipeline_service_spec.rb b/spec/services/google_cloud/generate_pipeline_service_spec.rb
index 26a1ccb7e3b..8f49e1af901 100644
--- a/spec/services/google_cloud/generate_pipeline_service_spec.rb
+++ b/spec/services/google_cloud/generate_pipeline_service_spec.rb
@@ -32,7 +32,7 @@ RSpec.describe GoogleCloud::GeneratePipelineService, feature_category: :deployme
response = service.execute
ref = response[:commit][:result]
- gitlab_ci_yml = project.repository.gitlab_ci_yml_for(ref)
+ gitlab_ci_yml = project.ci_config_for(ref)
expect(response[:status]).to eq(:success)
expect(gitlab_ci_yml).to include('https://gitlab.com/gitlab-org/incubation-engineering/five-minute-production/library/-/raw/main/gcp/cloud-run.gitlab-ci.yml')
@@ -97,7 +97,7 @@ EOF
response = service.execute
branch_name = response[:branch_name]
- gitlab_ci_yml = project.repository.gitlab_ci_yml_for(branch_name)
+ gitlab_ci_yml = project.ci_config_for(branch_name)
pipeline = Gitlab::Config::Loader::Yaml.new(gitlab_ci_yml).load!
expect(response[:status]).to eq(:success)
@@ -110,7 +110,7 @@ EOF
response = service.execute
branch_name = response[:branch_name]
- gitlab_ci_yml = project.repository.gitlab_ci_yml_for(branch_name)
+ gitlab_ci_yml = project.ci_config_for(branch_name)
expect(YAML.safe_load(gitlab_ci_yml).keys).to eq(%w[stages build-java test-java include])
end
@@ -153,7 +153,7 @@ EOF
response = service.execute
branch_name = response[:branch_name]
- gitlab_ci_yml = project.repository.gitlab_ci_yml_for(branch_name)
+ gitlab_ci_yml = project.ci_config_for(branch_name)
pipeline = Gitlab::Config::Loader::Yaml.new(gitlab_ci_yml).load!
expect(response[:status]).to eq(:success)
@@ -195,7 +195,7 @@ EOF
response = service.execute
branch_name = response[:branch_name]
- gitlab_ci_yml = project.repository.gitlab_ci_yml_for(branch_name)
+ gitlab_ci_yml = project.ci_config_for(branch_name)
pipeline = Gitlab::Config::Loader::Yaml.new(gitlab_ci_yml).load!
expect(response[:status]).to eq(:success)
@@ -235,7 +235,7 @@ EOF
response = service.execute
ref = response[:commit][:result]
- gitlab_ci_yml = project.repository.gitlab_ci_yml_for(ref)
+ gitlab_ci_yml = project.ci_config_for(ref)
expect(response[:status]).to eq(:success)
expect(gitlab_ci_yml).to include('https://gitlab.com/gitlab-org/incubation-engineering/five-minute-production/library/-/raw/main/gcp/cloud-storage.gitlab-ci.yml')
@@ -272,7 +272,7 @@ EOF
response = service.execute
ref = response[:commit][:result]
- gitlab_ci_yml = project.repository.gitlab_ci_yml_for(ref)
+ gitlab_ci_yml = project.ci_config_for(ref)
expect(response[:status]).to eq(:success)
expect(gitlab_ci_yml).to include('https://gitlab.com/gitlab-org/incubation-engineering/five-minute-production/library/-/raw/main/gcp/vision-ai.gitlab-ci.yml')
@@ -328,7 +328,7 @@ EOF
response = service.execute
branch_name = response[:branch_name]
- gitlab_ci_yml = project.repository.gitlab_ci_yml_for(branch_name)
+ gitlab_ci_yml = project.ci_config_for(branch_name)
pipeline = Gitlab::Config::Loader::Yaml.new(gitlab_ci_yml).load!
expect(response[:status]).to eq(:success)
diff --git a/spec/services/groups/update_statistics_service_spec.rb b/spec/services/groups/update_statistics_service_spec.rb
index 6bab36eca89..39b9c1c234d 100644
--- a/spec/services/groups/update_statistics_service_spec.rb
+++ b/spec/services/groups/update_statistics_service_spec.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe Groups::UpdateStatisticsService, feature_category: :groups_and_projects do
let_it_be(:group, reload: true) { create(:group) }
- let(:statistics) { %w(wiki_size) }
+ let(:statistics) { %w[wiki_size] }
subject(:service) { described_class.new(group, statistics: statistics) }
diff --git a/spec/services/import/gitlab_projects/create_project_service_spec.rb b/spec/services/import/gitlab_projects/create_project_service_spec.rb
index a77e9bdfce1..3f5dc7a928f 100644
--- a/spec/services/import/gitlab_projects/create_project_service_spec.rb
+++ b/spec/services/import/gitlab_projects/create_project_service_spec.rb
@@ -144,9 +144,9 @@ RSpec.describe ::Import::GitlabProjects::CreateProjectService, :aggregate_failur
)
expect(response.payload).to eq(
other_errors: [
- %{Project namespace path can contain only letters, digits, '_', '-' and '.'. Cannot start with '-', end in '.git' or end in '.atom'},
- %{Path can contain only letters, digits, '_', '-' and '.'. Cannot start with '-', end in '.git' or end in '.atom'},
- %{Path must not start or end with a special character and must not contain consecutive special characters.}
+ %(Project namespace path can contain only letters, digits, '_', '-' and '.'. Cannot start with '-', end in '.git' or end in '.atom'),
+ %(Path can contain only letters, digits, '_', '-' and '.'. Cannot start with '-', end in '.git' or end in '.atom'),
+ %(Path must not start or end with a special character and must not contain consecutive special characters.)
])
end
end
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 1d2b3975832..15e80f2c85d 100644
--- a/spec/services/import/validate_remote_git_endpoint_service_spec.rb
+++ b/spec/services/import/validate_remote_git_endpoint_service_spec.rb
@@ -7,7 +7,9 @@ RSpec.describe Import::ValidateRemoteGitEndpointService, feature_category: :impo
let_it_be(:base_url) { 'http://demo.host/path' }
let_it_be(:endpoint_url) { "#{base_url}/info/refs?service=git-upload-pack" }
- let_it_be(:error_message) { "#{base_url} is not a valid HTTP Git repository" }
+ let_it_be(:endpoint_error_message) { "#{base_url} endpoint error:" }
+ let_it_be(:body_error_message) { described_class::INVALID_BODY_MESSAGE }
+ let_it_be(:content_type_error_message) { described_class::INVALID_CONTENT_TYPE_MESSAGE }
describe '#execute' do
let(:valid_response) do
@@ -70,13 +72,14 @@ RSpec.describe Import::ValidateRemoteGitEndpointService, feature_category: :impo
end
it 'reports error when status code is not 200' do
- stub_full_request(endpoint_url, method: :get).to_return(valid_response.merge({ status: 301 }))
+ error_response = { status: 401 }
+ stub_full_request(endpoint_url, method: :get).to_return(error_response)
result = subject.execute
expect(result).to be_a(ServiceResponse)
expect(result.error?).to be(true)
- expect(result.message).to eq(error_message)
+ expect(result.message).to eq("#{endpoint_error_message} #{error_response[:status]}")
end
it 'reports error when invalid URL is provided' do
@@ -94,27 +97,49 @@ RSpec.describe Import::ValidateRemoteGitEndpointService, feature_category: :impo
expect(result).to be_a(ServiceResponse)
expect(result.error?).to be(true)
- expect(result.message).to eq(error_message)
+ expect(result.message).to eq(content_type_error_message)
end
- it 'reports error when body is in invalid format' do
+ it 'reports error when body is too short' do
stub_full_request(endpoint_url, method: :get).to_return(valid_response.merge({ body: 'invalid content' }))
result = subject.execute
expect(result).to be_a(ServiceResponse)
expect(result.error?).to be(true)
- expect(result.message).to eq(error_message)
+ expect(result.message).to eq(body_error_message)
+ end
+
+ it 'reports error when body is in invalid format' do
+ stub_full_request(endpoint_url, method: :get).to_return(valid_response.merge({ body: 'invalid long content with no git respons whatshowever' }))
+
+ result = subject.execute
+
+ expect(result).to be_a(ServiceResponse)
+ expect(result.error?).to be(true)
+ expect(result.message).to eq(body_error_message)
+ end
+
+ it 'reports error when http exceptions are raised' do
+ err = SocketError.new('dummy message')
+ stub_full_request(endpoint_url, method: :get).to_raise(err)
+
+ result = subject.execute
+
+ expect(result).to be_a(ServiceResponse)
+ expect(result.error?).to be(true)
+ expect(result.message).to eq("HTTP #{err.class.name.underscore} error: #{err.message}")
end
- it 'reports error when exception is raised' do
- stub_full_request(endpoint_url, method: :get).to_raise(SocketError.new('dummy message'))
+ it 'reports error when other exceptions are raised' do
+ err = StandardError.new('internal dummy message')
+ stub_full_request(endpoint_url, method: :get).to_raise(err)
result = subject.execute
expect(result).to be_a(ServiceResponse)
expect(result.error?).to be(true)
- expect(result.message).to eq(error_message)
+ expect(result.message).to eq("Internal #{err.class.name.underscore} error: #{err.message}")
end
end
diff --git a/spec/services/issuable/common_system_notes_service_spec.rb b/spec/services/issuable/common_system_notes_service_spec.rb
index 9306aeaac44..3d83c9ec9c2 100644
--- a/spec/services/issuable/common_system_notes_service_spec.rb
+++ b/spec/services/issuable/common_system_notes_service_spec.rb
@@ -42,7 +42,7 @@ RSpec.describe Issuable::CommonSystemNotesService, feature_category: :team_plann
context 'on issuable update' do
it_behaves_like 'system note creation', { title: 'New title' }, 'changed title'
it_behaves_like 'system note creation', { description: 'New description' }, 'changed the description'
- it_behaves_like 'system note creation', { discussion_locked: true }, 'locked this issue'
+ it_behaves_like 'system note creation', { discussion_locked: true }, 'locked the discussion in this issue'
it_behaves_like 'system note creation', { time_estimate: 5 }, 'changed time estimate'
context 'when new label is added' do
diff --git a/spec/services/issuable/discussions_list_service_spec.rb b/spec/services/issuable/discussions_list_service_spec.rb
index 446cc286e28..9c791ce9cd3 100644
--- a/spec/services/issuable/discussions_list_service_spec.rb
+++ b/spec/services/issuable/discussions_list_service_spec.rb
@@ -30,6 +30,12 @@ RSpec.describe Issuable::DiscussionsListService, feature_category: :team_plannin
expect(discussions_service.execute).to be_empty
end
end
+
+ context 'when issue exists at the group level' do
+ let_it_be(:issuable) { create(:issue, :group_level, namespace: group) }
+
+ it_behaves_like 'listing issuable discussions', :guest, 1, 7
+ end
end
describe 'fetching notes for merge requests' do
diff --git a/spec/services/issuable/process_assignees_spec.rb b/spec/services/issuable/process_assignees_spec.rb
index fac7ef9ce77..5484f46e955 100644
--- a/spec/services/issuable/process_assignees_spec.rb
+++ b/spec/services/issuable/process_assignees_spec.rb
@@ -6,11 +6,11 @@ RSpec.describe Issuable::ProcessAssignees, feature_category: :team_planning do
describe '#execute' do
it 'returns assignee_ids when add_assignee_ids and remove_assignee_ids are not specified' do
process = described_class.new(
- assignee_ids: %w(5 7 9),
+ assignee_ids: %w[5 7 9],
add_assignee_ids: nil,
remove_assignee_ids: nil,
- existing_assignee_ids: %w(1 3 9),
- extra_assignee_ids: %w(2 5 12)
+ existing_assignee_ids: %w[1 3 9],
+ extra_assignee_ids: %w[2 5 12]
)
result = process.execute
@@ -22,8 +22,8 @@ RSpec.describe Issuable::ProcessAssignees, feature_category: :team_planning do
assignee_ids: nil,
add_assignee_ids: nil,
remove_assignee_ids: nil,
- existing_assignee_ids: %w(1 3 11),
- extra_assignee_ids: %w(2 5 12)
+ existing_assignee_ids: %w[1 3 11],
+ extra_assignee_ids: %w[2 5 12]
)
result = process.execute
@@ -32,11 +32,11 @@ RSpec.describe Issuable::ProcessAssignees, feature_category: :team_planning do
it 'combines other ids when both add_assignee_ids and remove_assignee_ids are not empty' do
process = described_class.new(
- assignee_ids: %w(5 7 9),
- add_assignee_ids: %w(2 4 6),
- remove_assignee_ids: %w(4 7 11),
- existing_assignee_ids: %w(1 3 11),
- extra_assignee_ids: %w(2 5 12)
+ assignee_ids: %w[5 7 9],
+ add_assignee_ids: %w[2 4 6],
+ remove_assignee_ids: %w[4 7 11],
+ existing_assignee_ids: %w[1 3 11],
+ extra_assignee_ids: %w[2 5 12]
)
result = process.execute
@@ -45,11 +45,11 @@ RSpec.describe Issuable::ProcessAssignees, feature_category: :team_planning do
it 'combines other ids when remove_assignee_ids is not empty' do
process = described_class.new(
- assignee_ids: %w(5 7 9),
+ assignee_ids: %w[5 7 9],
add_assignee_ids: nil,
- remove_assignee_ids: %w(4 7 11),
- existing_assignee_ids: %w(1 3 11),
- extra_assignee_ids: %w(2 5 12)
+ remove_assignee_ids: %w[4 7 11],
+ existing_assignee_ids: %w[1 3 11],
+ extra_assignee_ids: %w[2 5 12]
)
result = process.execute
@@ -58,11 +58,11 @@ RSpec.describe Issuable::ProcessAssignees, feature_category: :team_planning do
it 'combines other ids when add_assignee_ids is not empty' do
process = described_class.new(
- assignee_ids: %w(5 7 9),
- add_assignee_ids: %w(2 4 6),
+ assignee_ids: %w[5 7 9],
+ add_assignee_ids: %w[2 4 6],
remove_assignee_ids: nil,
- existing_assignee_ids: %w(1 3 11),
- extra_assignee_ids: %w(2 5 12)
+ existing_assignee_ids: %w[1 3 11],
+ extra_assignee_ids: %w[2 5 12]
)
result = process.execute
@@ -71,9 +71,9 @@ RSpec.describe Issuable::ProcessAssignees, feature_category: :team_planning do
it 'combines ids when existing_assignee_ids and extra_assignee_ids are omitted' do
process = described_class.new(
- assignee_ids: %w(5 7 9),
- add_assignee_ids: %w(2 4 6),
- remove_assignee_ids: %w(4 7 11)
+ assignee_ids: %w[5 7 9],
+ add_assignee_ids: %w[2 4 6],
+ remove_assignee_ids: %w[4 7 11]
)
result = process.execute
@@ -82,11 +82,11 @@ RSpec.describe Issuable::ProcessAssignees, feature_category: :team_planning do
it 'handles mixed string and integer arrays' do
process = described_class.new(
- assignee_ids: %w(5 7 9),
+ assignee_ids: %w[5 7 9],
add_assignee_ids: [2, 4, 6],
- remove_assignee_ids: %w(4 7 11),
+ remove_assignee_ids: %w[4 7 11],
existing_assignee_ids: [1, 3, 11],
- extra_assignee_ids: %w(2 5 12)
+ extra_assignee_ids: %w[2 5 12]
)
result = process.execute
diff --git a/spec/services/issues/export_csv_service_spec.rb b/spec/services/issues/export_csv_service_spec.rb
index 31eaa72255d..83dfca923fb 100644
--- a/spec/services/issues/export_csv_service_spec.rb
+++ b/spec/services/issues/export_csv_service_spec.rb
@@ -160,7 +160,7 @@ RSpec.describe Issues::ExportCsvService, :with_license, feature_category: :team_
context 'with issues filtered by labels and project' do
subject do
described_class.new(
- IssuesFinder.new(user, project_id: project.id, label_name: %w(Idea Feature)).execute,
+ IssuesFinder.new(user, project_id: project.id, label_name: %w[Idea Feature]).execute,
project
)
end
diff --git a/spec/services/issues/update_service_spec.rb b/spec/services/issues/update_service_spec.rb
index c4012e2a98f..0cb13bfb917 100644
--- a/spec/services/issues/update_service_spec.rb
+++ b/spec/services/issues/update_service_spec.rb
@@ -491,9 +491,9 @@ RSpec.describe Issues::UpdateService, :mailer, feature_category: :team_planning
end
it 'creates system note about discussion lock' do
- note = find_note('locked this issue')
+ note = find_note('locked the discussion in this issue')
- expect(note.note).to eq 'locked this issue'
+ expect(note.note).to eq 'locked the discussion in this issue'
end
end
@@ -539,21 +539,6 @@ RSpec.describe Issues::UpdateService, :mailer, feature_category: :team_planning
end
end
end
-
- it 'verifies the number of queries' do
- update_issue(description: "- [ ] Task 1 #{user.to_reference}")
-
- baseline = ActiveRecord::QueryRecorder.new do
- update_issue(description: "- [x] Task 1 #{user.to_reference}")
- end
-
- recorded = ActiveRecord::QueryRecorder.new do
- update_issue(description: "- [x] Task 1 #{user.to_reference}\n- [ ] Task 2 #{user.to_reference}")
- end
-
- expect(recorded.count).to eq(baseline.count)
- expect(recorded.cached_count).to eq(0)
- end
end
context 'when description changed' do
diff --git a/spec/services/jira/requests/projects/list_service_spec.rb b/spec/services/jira/requests/projects/list_service_spec.rb
index f9e3a3e8510..d8e6eda2dd4 100644
--- a/spec/services/jira/requests/projects/list_service_spec.rb
+++ b/spec/services/jira/requests/projects/list_service_spec.rb
@@ -73,7 +73,7 @@ RSpec.describe Jira::Requests::Projects::ListService, feature_category: :groups_
payload = subject.payload
expect(subject.success?).to be_truthy
- expect(payload[:projects].map(&:key)).to eq(%w(pr1 pr2))
+ expect(payload[:projects].map(&:key)).to eq(%w[pr1 pr2])
expect(payload[:is_last]).to be_truthy
end
@@ -84,7 +84,7 @@ RSpec.describe Jira::Requests::Projects::ListService, feature_category: :groups_
payload = subject.payload
expect(subject.success?).to be_truthy
- expect(payload[:projects].map(&:key)).to eq(%w(pr1))
+ expect(payload[:projects].map(&:key)).to eq(%w[pr1])
expect(payload[:is_last]).to be_truthy
end
end
diff --git a/spec/services/jira_connect_subscriptions/create_service_spec.rb b/spec/services/jira_connect_subscriptions/create_service_spec.rb
index f9d3954b84c..2296d0fbfed 100644
--- a/spec/services/jira_connect_subscriptions/create_service_spec.rb
+++ b/spec/services/jira_connect_subscriptions/create_service_spec.rb
@@ -9,7 +9,7 @@ RSpec.describe JiraConnectSubscriptions::CreateService, feature_category: :integ
let(:path) { group.full_path }
let(:params) { { namespace_path: path, jira_user: jira_user } }
- let(:jira_user) { double(:JiraUser, site_admin?: true) }
+ let(:jira_user) { double(:JiraUser, jira_admin?: true) }
subject { described_class.new(installation, current_user, params).execute }
@@ -29,11 +29,11 @@ RSpec.describe JiraConnectSubscriptions::CreateService, feature_category: :integ
end
context 'remote user does not have access' do
- let(:jira_user) { double(site_admin?: false) }
+ let(:jira_user) { double(jira_admin?: false) }
it_behaves_like 'a failed execution',
http_status: 403,
- message: 'The Jira user is not a site administrator. Check the permissions in Jira and try again.'
+ message: 'The Jira user is not a site or organization administrator. Check the permissions in Jira and try again.'
end
context 'remote user cannot be retrieved' do
diff --git a/spec/services/lfs/file_transformer_spec.rb b/spec/services/lfs/file_transformer_spec.rb
index c90d7af022f..398beabbeeb 100644
--- a/spec/services/lfs/file_transformer_spec.rb
+++ b/spec/services/lfs/file_transformer_spec.rb
@@ -218,7 +218,7 @@ RSpec.describe Lfs::FileTransformer, feature_category: :source_code_management d
repository_types = project.lfs_objects_projects.order(:id).pluck(:repository_type)
- expect(repository_types).to eq(%w(project wiki))
+ expect(repository_types).to eq(%w[project wiki])
end
end
end
diff --git a/spec/services/merge_requests/conflicts/resolve_service_spec.rb b/spec/services/merge_requests/conflicts/resolve_service_spec.rb
index 002a07ff14e..a4245456367 100644
--- a/spec/services/merge_requests/conflicts/resolve_service_spec.rb
+++ b/spec/services/merge_requests/conflicts/resolve_service_spec.rb
@@ -72,8 +72,8 @@ RSpec.describe MergeRequests::Conflicts::ResolveService, feature_category: :code
it 'creates a commit with the correct parents' do
expect(merge_request.source_branch_head.parents.map(&:id))
- .to eq(%w(1450cd639e0bc6721eb02800169e464f212cde06
- 824be604a34828eb682305f0d963056cfac87b2d))
+ .to eq(%w[1450cd639e0bc6721eb02800169e464f212cde06
+ 824be604a34828eb682305f0d963056cfac87b2d])
end
end
@@ -169,8 +169,8 @@ RSpec.describe MergeRequests::Conflicts::ResolveService, feature_category: :code
it 'creates a commit with the correct parents' do
expect(merge_request.source_branch_head.parents.map(&:id))
- .to eq(%w(1450cd639e0bc6721eb02800169e464f212cde06
- 824be604a34828eb682305f0d963056cfac87b2d))
+ .to eq(%w[1450cd639e0bc6721eb02800169e464f212cde06
+ 824be604a34828eb682305f0d963056cfac87b2d])
end
it 'sets the content to the content given' do
diff --git a/spec/services/merge_requests/mark_reviewer_reviewed_service_spec.rb b/spec/services/merge_requests/mark_reviewer_reviewed_service_spec.rb
deleted file mode 100644
index 172c2133168..00000000000
--- a/spec/services/merge_requests/mark_reviewer_reviewed_service_spec.rb
+++ /dev/null
@@ -1,55 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe MergeRequests::MarkReviewerReviewedService, feature_category: :code_review_workflow do
- let(:current_user) { create(:user) }
- let(:merge_request) { create(:merge_request, reviewers: [current_user]) }
- let(:reviewer) { merge_request.merge_request_reviewers.find_by(user_id: current_user.id) }
- let(:project) { merge_request.project }
- let(:service) { described_class.new(project: project, current_user: current_user) }
- let(:result) { service.execute(merge_request) }
-
- before do
- project.add_developer(current_user)
- end
-
- describe '#execute' do
- shared_examples_for 'failed service execution' do
- it 'returns an error' do
- expect(result[:status]).to eq :error
- end
-
- it_behaves_like 'does not trigger GraphQL subscription mergeRequestReviewersUpdated' do
- let(:action) { result }
- end
- end
-
- describe 'invalid permissions' do
- let(:service) { described_class.new(project: project, current_user: create(:user)) }
-
- it_behaves_like 'failed service execution'
- end
-
- describe 'reviewer does not exist' do
- let(:service) { described_class.new(project: project, current_user: create(:user)) }
-
- it_behaves_like 'failed service execution'
- end
-
- describe 'reviewer exists' do
- it 'returns success' do
- expect(result[:status]).to eq :success
- end
-
- it 'updates reviewers state' do
- expect(result[:status]).to eq :success
- expect(reviewer.state).to eq 'reviewed'
- end
-
- it_behaves_like 'triggers GraphQL subscription mergeRequestReviewersUpdated' do
- let(:action) { result }
- end
- end
- end
-end
diff --git a/spec/services/merge_requests/merge_service_spec.rb b/spec/services/merge_requests/merge_service_spec.rb
index 6e34f4362c1..2e8f0049f28 100644
--- a/spec/services/merge_requests/merge_service_spec.rb
+++ b/spec/services/merge_requests/merge_service_spec.rb
@@ -569,7 +569,7 @@ RSpec.describe MergeRequests::MergeService, feature_category: :code_review_workf
allow_any_instance_of(Repository).to receive(:ancestor?).and_return(nil)
end
- %w(semi-linear ff).each do |merge_method|
+ %w[semi-linear ff].each do |merge_method|
it "logs and saves error if merge is #{merge_method} only" do
merge_method = 'rebase_merge' if merge_method == 'semi-linear'
merge_request.project.update!(merge_method: merge_method)
@@ -599,6 +599,7 @@ RSpec.describe MergeRequests::MergeService, feature_category: :code_review_workf
context 'with failing CI' do
before do
+ allow(merge_request.project).to receive(:only_allow_merge_if_pipeline_succeeds) { true }
allow(merge_request).to receive(:mergeable_ci_state?) { false }
end
@@ -616,6 +617,7 @@ RSpec.describe MergeRequests::MergeService, feature_category: :code_review_workf
context 'with unresolved discussions' do
before do
+ allow(merge_request.project).to receive(:only_allow_merge_if_all_discussions_are_resolved) { true }
allow(merge_request).to receive(:mergeable_discussions_state?) { false }
end
diff --git a/spec/services/merge_requests/mergeability/check_ci_status_service_spec.rb b/spec/services/merge_requests/mergeability/check_ci_status_service_spec.rb
index cf835cf70a3..067e87859e7 100644
--- a/spec/services/merge_requests/mergeability/check_ci_status_service_spec.rb
+++ b/spec/services/merge_requests/mergeability/check_ci_status_service_spec.rb
@@ -5,7 +5,8 @@ require 'spec_helper'
RSpec.describe MergeRequests::Mergeability::CheckCiStatusService, feature_category: :code_review_workflow do
subject(:check_ci_status) { described_class.new(merge_request: merge_request, params: params) }
- let(:merge_request) { build(:merge_request) }
+ let_it_be(:project) { build(:project) }
+ let_it_be(:merge_request) { build(:merge_request, source_project: project) }
let(:params) { { skip_ci_check: skip_check } }
let(:skip_check) { false }
@@ -13,23 +14,41 @@ RSpec.describe MergeRequests::Mergeability::CheckCiStatusService, feature_catego
let(:result) { check_ci_status.execute }
before do
- expect(merge_request).to receive(:mergeable_ci_state?).and_return(mergeable)
+ allow(merge_request)
+ .to receive(:only_allow_merge_if_pipeline_succeeds?)
+ .and_return(only_allow_merge_if_pipeline_succeeds?)
end
- context 'when the merge request is in a mergable state' do
- let(:mergeable) { true }
+ context 'when only_allow_merge_if_pipeline_succeeds is true' do
+ let(:only_allow_merge_if_pipeline_succeeds?) { true }
- it 'returns a check result with status success' do
- expect(result.status).to eq Gitlab::MergeRequests::Mergeability::CheckResult::SUCCESS_STATUS
+ before do
+ expect(merge_request).to receive(:mergeable_ci_state?).and_return(mergeable)
+ end
+
+ context 'when the merge request is in a mergeable state' do
+ let(:mergeable) { true }
+
+ it 'returns a check result with status success' do
+ expect(result.status).to eq Gitlab::MergeRequests::Mergeability::CheckResult::SUCCESS_STATUS
+ end
+ end
+
+ context 'when the merge request is not in a mergeable state' do
+ let(:mergeable) { false }
+
+ it 'returns a check result with status failed' do
+ expect(result.status).to eq Gitlab::MergeRequests::Mergeability::CheckResult::FAILED_STATUS
+ expect(result.payload[:reason]).to eq :ci_must_pass
+ end
end
end
- context 'when the merge request is not in a mergeable state' do
- let(:mergeable) { false }
+ context 'when only_allow_merge_if_pipeline_succeeds is false' do
+ let(:only_allow_merge_if_pipeline_succeeds?) { false }
- it 'returns a check result with status failed' do
- expect(result.status).to eq Gitlab::MergeRequests::Mergeability::CheckResult::FAILED_STATUS
- expect(result.payload[:reason]).to eq :ci_must_pass
+ it 'returns a check result with inactive status' do
+ expect(result.status).to eq Gitlab::MergeRequests::Mergeability::CheckResult::INACTIVE_STATUS
end
end
end
diff --git a/spec/services/merge_requests/mergeability/check_discussions_status_service_spec.rb b/spec/services/merge_requests/mergeability/check_discussions_status_service_spec.rb
index a3b77558ec3..4a8b28f603d 100644
--- a/spec/services/merge_requests/mergeability/check_discussions_status_service_spec.rb
+++ b/spec/services/merge_requests/mergeability/check_discussions_status_service_spec.rb
@@ -5,7 +5,8 @@ require 'spec_helper'
RSpec.describe MergeRequests::Mergeability::CheckDiscussionsStatusService, feature_category: :code_review_workflow do
subject(:check_discussions_status) { described_class.new(merge_request: merge_request, params: params) }
- let(:merge_request) { build(:merge_request) }
+ let_it_be(:project) { build(:project) }
+ let_it_be(:merge_request) { build(:merge_request, source_project: project) }
let(:params) { { skip_discussions_check: skip_check } }
let(:skip_check) { false }
@@ -13,23 +14,41 @@ RSpec.describe MergeRequests::Mergeability::CheckDiscussionsStatusService, featu
let(:result) { check_discussions_status.execute }
before do
- expect(merge_request).to receive(:mergeable_discussions_state?).and_return(mergeable)
+ allow(merge_request)
+ .to receive(:only_allow_merge_if_all_discussions_are_resolved?)
+ .and_return(only_allow_merge_if_all_discussions_are_resolved?)
end
- context 'when the merge request is in a mergable state' do
- let(:mergeable) { true }
+ context 'when only_allow_merge_if_all_discussions_are_resolved is true' do
+ let(:only_allow_merge_if_all_discussions_are_resolved?) { true }
- it 'returns a check result with status success' do
- expect(result.status).to eq Gitlab::MergeRequests::Mergeability::CheckResult::SUCCESS_STATUS
+ before do
+ allow(merge_request).to receive(:mergeable_discussions_state?).and_return(mergeable)
+ end
+
+ context 'when the merge request is in a mergable state' do
+ let(:mergeable) { true }
+
+ it 'returns a check result with status success' do
+ expect(result.status).to eq Gitlab::MergeRequests::Mergeability::CheckResult::SUCCESS_STATUS
+ end
+ end
+
+ context 'when the merge request is not in a mergeable state' do
+ let(:mergeable) { false }
+
+ it 'returns a check result with status failed' do
+ expect(result.status).to eq Gitlab::MergeRequests::Mergeability::CheckResult::FAILED_STATUS
+ expect(result.payload[:reason]).to eq(:discussions_not_resolved)
+ end
end
end
- context 'when the merge request is not in a mergeable state' do
- let(:mergeable) { false }
+ context 'when only_allow_merge_if_all_discussions_are_resolved is false' do
+ let(:only_allow_merge_if_all_discussions_are_resolved?) { false }
- it 'returns a check result with status failed' do
- expect(result.status).to eq Gitlab::MergeRequests::Mergeability::CheckResult::FAILED_STATUS
- expect(result.payload[:reason]).to eq(:discussions_not_resolved)
+ it 'returns a check result with inactive status' do
+ expect(result.status).to eq Gitlab::MergeRequests::Mergeability::CheckResult::INACTIVE_STATUS
end
end
end
diff --git a/spec/services/merge_requests/mergeability/check_rebase_status_service_spec.rb b/spec/services/merge_requests/mergeability/check_rebase_status_service_spec.rb
index 31ec44856b1..d6948f72c0a 100644
--- a/spec/services/merge_requests/mergeability/check_rebase_status_service_spec.rb
+++ b/spec/services/merge_requests/mergeability/check_rebase_status_service_spec.rb
@@ -5,7 +5,8 @@ require 'spec_helper'
RSpec.describe MergeRequests::Mergeability::CheckRebaseStatusService, feature_category: :code_review_workflow do
subject(:check_rebase_status) { described_class.new(merge_request: merge_request, params: params) }
- let(:merge_request) { build(:merge_request) }
+ let_it_be(:project) { build(:project) }
+ let_it_be(:merge_request) { build(:merge_request, source_project: project) }
let(:params) { { skip_rebase_check: skip_check } }
let(:skip_check) { false }
@@ -13,23 +14,41 @@ RSpec.describe MergeRequests::Mergeability::CheckRebaseStatusService, feature_ca
let(:result) { check_rebase_status.execute }
before do
- allow(merge_request).to receive(:should_be_rebased?).and_return(should_be_rebased)
+ allow(project)
+ .to receive(:ff_merge_must_be_possible?)
+ .and_return(ff_merge_must_be_possible?)
end
- context 'when the merge request should be rebased' do
- let(:should_be_rebased) { true }
+ context 'when ff_merge_must_be_possible is true' do
+ let(:ff_merge_must_be_possible?) { true }
- it 'returns a check result with status failed' do
- expect(result.status).to eq Gitlab::MergeRequests::Mergeability::CheckResult::FAILED_STATUS
- expect(result.payload[:reason]).to eq :need_rebase
+ before do
+ allow(merge_request).to receive(:should_be_rebased?).and_return(should_be_rebased)
+ end
+
+ context 'when the merge request should be rebased' do
+ let(:should_be_rebased) { true }
+
+ it 'returns a check result with status failed' do
+ expect(result.status).to eq Gitlab::MergeRequests::Mergeability::CheckResult::FAILED_STATUS
+ expect(result.payload[:reason]).to eq(:need_rebase)
+ end
+ end
+
+ context 'when the merge request should not be rebased' do
+ let(:should_be_rebased) { false }
+
+ it 'returns a check result with status success' do
+ expect(result.status).to eq Gitlab::MergeRequests::Mergeability::CheckResult::SUCCESS_STATUS
+ end
end
end
- context 'when the merge request should not be rebased' do
- let(:should_be_rebased) { false }
+ context 'when ff_merge_must_be_possible is false' do
+ let(:ff_merge_must_be_possible?) { false }
- it 'returns a check result with status success' do
- expect(result.status).to eq Gitlab::MergeRequests::Mergeability::CheckResult::SUCCESS_STATUS
+ it 'returns a check result with inactive status' do
+ expect(result.status).to eq Gitlab::MergeRequests::Mergeability::CheckResult::INACTIVE_STATUS
end
end
end
diff --git a/spec/services/merge_requests/mergeability/run_checks_service_spec.rb b/spec/services/merge_requests/mergeability/run_checks_service_spec.rb
index 546d583a2fb..06e15356a92 100644
--- a/spec/services/merge_requests/mergeability/run_checks_service_spec.rb
+++ b/spec/services/merge_requests/mergeability/run_checks_service_spec.rb
@@ -98,6 +98,26 @@ RSpec.describe MergeRequests::Mergeability::RunChecksService, :clean_gitlab_redi
let(:expected_count) { checks.count - 1 }
end
end
+
+ context 'when one check is inactive' do
+ let(:inactive_result) { Gitlab::MergeRequests::Mergeability::CheckResult.inactive }
+
+ before do
+ allow_next_instance_of(MergeRequests::Mergeability::CheckOpenStatusService) do |service|
+ allow(service).to receive(:skip?).and_return(false)
+ allow(service).to receive(:execute).and_return(inactive_result)
+ end
+ end
+
+ it 'is still a success' do
+ expect(execute.success?).to eq(true)
+ end
+
+ it_behaves_like 'checks are all executed' do
+ let(:success?) { true }
+ let(:expected_count) { checks.count - 1 }
+ end
+ end
end
context 'when a check is not skipped' do
diff --git a/spec/services/merge_requests/push_options_handler_service_spec.rb b/spec/services/merge_requests/push_options_handler_service_spec.rb
index 49ec8b09939..038977e4fd0 100644
--- a/spec/services/merge_requests/push_options_handler_service_spec.rb
+++ b/spec/services/merge_requests/push_options_handler_service_spec.rb
@@ -54,6 +54,17 @@ RSpec.describe MergeRequests::PushOptionsHandlerService, feature_category: :sour
end
end
+ shared_examples_for 'a service that can set the target project of a merge request' do
+ subject(:last_mr) { MergeRequest.last }
+
+ it 'creates a merge request with the correct target project' do
+ project_path = push_options[:target_project] || project.default_merge_request_target.full_path
+
+ expect { service.execute }.to change { MergeRequest.count }.by(1)
+ expect(last_mr.target_project.full_path).to eq(project_path)
+ end
+ end
+
shared_examples_for 'a service that can set the title of a merge request' do
subject(:last_mr) { MergeRequest.last }
@@ -347,6 +358,31 @@ RSpec.describe MergeRequests::PushOptionsHandlerService, feature_category: :sour
it_behaves_like 'with the project default branch'
end
+ describe '`target_project` push option' do
+ let(:changes) { new_branch_changes }
+ let(:double_forked_project) { fork_project(forked_project, user1, repository: true) }
+ let(:service) { described_class.new(project: double_forked_project, current_user: user1, changes: changes, push_options: push_options) }
+ let(:push_options) { { create: true, target_project: target_project.full_path } }
+
+ context 'to self' do
+ let(:target_project) { double_forked_project }
+
+ it_behaves_like 'a service that can set the target project of a merge request'
+ end
+
+ context 'to intermediate project' do
+ let(:target_project) { forked_project }
+
+ it_behaves_like 'a service that can set the target project of a merge request'
+ end
+
+ context 'to base project' do
+ let(:target_project) { project }
+
+ it_behaves_like 'a service that can set the target project of a merge request'
+ end
+ end
+
describe '`title` push option' do
let(:push_options) { { title: title } }
@@ -861,6 +897,17 @@ RSpec.describe MergeRequests::PushOptionsHandlerService, feature_category: :sour
end
end
+ describe 'when the target project does not exist' do
+ let(:push_options) { { create: true, target: 'my-branch', target_project: 'does-not-exist' } }
+ let(:changes) { default_branch_changes }
+
+ it 'records an error', :sidekiq_inline do
+ service.execute
+
+ expect(service.errors).to eq(["User access was denied"])
+ end
+ end
+
describe 'when user does not have access to target project' do
let(:push_options) { { create: true, target: 'my-branch' } }
let(:changes) { default_branch_changes }
@@ -890,6 +937,18 @@ RSpec.describe MergeRequests::PushOptionsHandlerService, feature_category: :sour
end
end
+ describe 'when projects are unrelated' do
+ let(:unrelated_project) { create(:project, :public, :repository, group: child_group) }
+ let(:push_options) { { create: true, target_project: unrelated_project.full_path } }
+ let(:changes) { new_branch_changes }
+
+ it 'records an error' do
+ service.execute
+
+ expect(service.errors).to eq(["Projects #{project.full_path} and #{unrelated_project.full_path} are not in the same network"])
+ end
+ end
+
describe 'when MR has ActiveRecord errors' do
let(:push_options) { { create: true } }
let(:changes) { new_branch_changes }
diff --git a/spec/services/merge_requests/pushed_branches_service_spec.rb b/spec/services/merge_requests/pushed_branches_service_spec.rb
index cb5d0a6bd25..de99fb244d3 100644
--- a/spec/services/merge_requests/pushed_branches_service_spec.rb
+++ b/spec/services/merge_requests/pushed_branches_service_spec.rb
@@ -8,7 +8,7 @@ RSpec.describe MergeRequests::PushedBranchesService, feature_category: :source_c
context 'when branches pushed' do
let(:pushed_branches) do
- %w(branch1 branch2 closed-branch1 closed-branch2 extra1 extra2).map do |branch|
+ %w[branch1 branch2 closed-branch1 closed-branch2 extra1 extra2].map do |branch|
{ ref: "refs/heads/#{branch}" }
end
end
@@ -31,7 +31,7 @@ RSpec.describe MergeRequests::PushedBranchesService, feature_category: :source_c
context 'when tags pushed' do
let(:pushed_branches) do
- %w(v10.0.0 v11.0.2 v12.1.0).map do |branch|
+ %w[v10.0.0 v11.0.2 v12.1.0].map do |branch|
{ ref: "refs/tags/#{branch}" }
end
end
diff --git a/spec/services/merge_requests/refresh_service_spec.rb b/spec/services/merge_requests/refresh_service_spec.rb
index d5b7b56ccdd..dd50dfa49e0 100644
--- a/spec/services/merge_requests/refresh_service_spec.rb
+++ b/spec/services/merge_requests/refresh_service_spec.rb
@@ -915,7 +915,7 @@ RSpec.describe MergeRequests::RefreshService, feature_category: :code_review_wor
context 'feature enabled' do
it "updates merge requests' merge_commit and merged_commit values", :aggregate_failures do
expect(Gitlab::BranchPushMergeCommitAnalyzer).to receive(:new).and_wrap_original do |original_method, commits|
- expect(commits.map(&:id)).to eq(%w{646ece5cfed840eca0a4feb21bcd6a81bb19bda3 29284d9bcc350bcae005872d0be6edd016e2efb5 5f82584f0a907f3b30cfce5bb8df371454a90051 8a994512e8c8f0dfcf22bb16df6e876be7a61036 689600b91aabec706e657e38ea706ece1ee8268f db46a1c5a5e474aa169b6cdb7a522d891bc4c5f9})
+ expect(commits.map(&:id)).to eq(%w[646ece5cfed840eca0a4feb21bcd6a81bb19bda3 29284d9bcc350bcae005872d0be6edd016e2efb5 5f82584f0a907f3b30cfce5bb8df371454a90051 8a994512e8c8f0dfcf22bb16df6e876be7a61036 689600b91aabec706e657e38ea706ece1ee8268f db46a1c5a5e474aa169b6cdb7a522d891bc4c5f9])
original_method.call(commits)
end
diff --git a/spec/services/merge_requests/update_reviewer_state_service_spec.rb b/spec/services/merge_requests/update_reviewer_state_service_spec.rb
new file mode 100644
index 00000000000..be24d95d7f1
--- /dev/null
+++ b/spec/services/merge_requests/update_reviewer_state_service_spec.rb
@@ -0,0 +1,85 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe MergeRequests::UpdateReviewerStateService, feature_category: :code_review_workflow do
+ let_it_be(:current_user) { create(:user) }
+ let_it_be(:merge_request) { create(:merge_request, reviewers: [current_user]) }
+ let(:reviewer) { merge_request.merge_request_reviewers.find_by(user_id: current_user.id) }
+ let(:project) { merge_request.project }
+ let(:service) { described_class.new(project: project, current_user: current_user) }
+ let(:state) { 'requested_changes' }
+ let(:result) { service.execute(merge_request, state) }
+
+ before do
+ project.add_developer(current_user)
+ end
+
+ describe '#execute' do
+ shared_examples_for 'failed service execution' do
+ it 'returns an error' do
+ expect(result[:status]).to eq :error
+ end
+
+ it_behaves_like 'does not trigger GraphQL subscription mergeRequestReviewersUpdated' do
+ let(:action) { result }
+ end
+ end
+
+ describe 'invalid permissions' do
+ let(:service) { described_class.new(project: project, current_user: create(:user)) }
+
+ it_behaves_like 'failed service execution'
+ end
+
+ describe 'reviewer exists' do
+ it 'returns success' do
+ expect(result[:status]).to eq :success
+ end
+
+ it 'updates reviewers state' do
+ expect(result[:status]).to eq :success
+ expect(reviewer.state).to eq 'requested_changes'
+ end
+
+ it 'does not call MergeRequests::RemoveApprovalService' do
+ expect(MergeRequests::RemoveApprovalService).not_to receive(:new)
+
+ expect(result[:status]).to eq :success
+ end
+
+ it_behaves_like 'triggers GraphQL subscription mergeRequestReviewersUpdated' do
+ let(:action) { result }
+ end
+
+ context 'when reviewer has approved' do
+ before do
+ create(:approval, user: current_user, merge_request: merge_request)
+ end
+
+ it 'removes approval when state is requested_changes' do
+ expect_next_instance_of(
+ MergeRequests::RemoveApprovalService,
+ project: project, current_user: current_user
+ ) do |service|
+ expect(service).to receive(:execute).with(merge_request).and_return({ success: true })
+ end
+
+ expect(result[:status]).to eq :success
+ end
+
+ it 'renders error when remove approval service fails' do
+ expect_next_instance_of(
+ MergeRequests::RemoveApprovalService,
+ project: project, current_user: current_user
+ ) do |service|
+ expect(service).to receive(:execute).with(merge_request).and_return(nil)
+ end
+
+ expect(result[:status]).to eq :error
+ expect(result[:message]).to eq "Failed to remove approval"
+ end
+ end
+ end
+ end
+end
diff --git a/spec/services/merge_requests/update_service_spec.rb b/spec/services/merge_requests/update_service_spec.rb
index f5494f429c3..53dd4263770 100644
--- a/spec/services/merge_requests/update_service_spec.rb
+++ b/spec/services/merge_requests/update_service_spec.rb
@@ -351,10 +351,10 @@ RSpec.describe MergeRequests::UpdateService, :mailer, feature_category: :code_re
end
it 'creates system note about discussion lock' do
- note = find_note('locked this merge request')
+ note = find_note('locked the discussion in this merge request')
expect(note).not_to be_nil
- expect(note.note).to eq 'locked this merge request'
+ expect(note.note).to eq 'locked the discussion in this merge request'
end
context 'when current user cannot admin issues in the project' do
diff --git a/spec/services/ml/create_candidate_service_spec.rb b/spec/services/ml/create_candidate_service_spec.rb
new file mode 100644
index 00000000000..fb3456b0bcc
--- /dev/null
+++ b/spec/services/ml/create_candidate_service_spec.rb
@@ -0,0 +1,57 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe ::Ml::CreateCandidateService, feature_category: :mlops do
+ describe '#execute' do
+ let_it_be(:model_version) { create(:ml_model_versions) }
+ let_it_be(:experiment) { create(:ml_experiments, project: model_version.project) }
+
+ let(:params) { {} }
+
+ subject(:candidate) { described_class.new(experiment, params).execute }
+
+ context 'with default parameters' do
+ it 'creates a candidate' do
+ expect { candidate }.to change { experiment.candidates.count }.by(1)
+ end
+
+ it 'gives a fake name' do
+ expect(candidate.name).to match(/[a-z]+-[a-z]+-[a-z]+-\d+/)
+ end
+
+ it 'sets the correct values', :aggregate_failures do
+ expect(candidate.start_time).to eq(0)
+ expect(candidate.experiment).to be(experiment)
+ expect(candidate.project).to be(experiment.project)
+ expect(candidate.user).to be_nil
+ end
+ end
+
+ context 'when parameters are passed' do
+ let(:params) do
+ {
+ start_time: 1234,
+ name: 'candidate_name',
+ model_version: model_version,
+ user: experiment.user
+ }
+ end
+
+ context 'with default parameters' do
+ it 'creates a candidate' do
+ expect { candidate }.to change { experiment.candidates.count }.by(1)
+ end
+
+ it 'sets the correct values', :aggregate_failures do
+ expect(candidate.start_time).to eq(1234)
+ expect(candidate.experiment).to be(experiment)
+ expect(candidate.project).to be(experiment.project)
+ expect(candidate.user).to be(experiment.user)
+ expect(candidate.name).to eq('candidate_name')
+ expect(candidate.model_version_id).to eq(model_version.id)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/services/ml/create_model_service_spec.rb b/spec/services/ml/create_model_service_spec.rb
new file mode 100644
index 00000000000..212f0940635
--- /dev/null
+++ b/spec/services/ml/create_model_service_spec.rb
@@ -0,0 +1,81 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe ::Ml::CreateModelService, feature_category: :mlops do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:existing_model) { create(:ml_models) }
+ let_it_be(:another_project) { create(:project) }
+ let_it_be(:description) { 'description' }
+ let_it_be(:metadata) { [] }
+
+ subject(:create_model) { described_class.new(project, name, user, description, metadata).execute }
+
+ describe '#execute' do
+ context 'when model name does not exist in the project' do
+ let(:name) { 'new_model' }
+ let(:project) { existing_model.project }
+
+ it 'creates a model', :aggregate_failures do
+ expect { create_model }.to change { Ml::Model.count }.by(1)
+
+ expect(create_model.name).to eq(name)
+ end
+ end
+
+ context 'when model name exists but project is different' do
+ let(:name) { existing_model.name }
+ let(:project) { another_project }
+
+ it 'creates a model', :aggregate_failures do
+ expect { create_model }.to change { Ml::Model.count }.by(1)
+
+ expect(create_model.name).to eq(name)
+ end
+ end
+
+ context 'when model with name exists' do
+ let(:name) { existing_model.name }
+ let(:project) { existing_model.project }
+
+ it 'raises an error', :aggregate_failures do
+ expect { create_model }.to raise_error(ActiveRecord::RecordInvalid)
+ end
+ end
+
+ context 'when metadata are supplied, add them as metadata' do
+ let(:name) { 'new_model' }
+ let(:project) { existing_model.project }
+ let(:metadata) { [{ key: 'key1', value: 'value1' }, { key: 'key2', value: 'value2' }] }
+
+ it 'creates metadata records', :aggregate_failures do
+ expect { create_model }.to change { Ml::Model.count }.by(1)
+
+ expect(create_model.name).to eq(name)
+ expect(create_model.metadata.count).to be 2
+ end
+ end
+
+ # TODO: Ensure consisted error responses https://gitlab.com/gitlab-org/gitlab/-/issues/429731
+ context 'for metadata with duplicate keys, it does not create duplicate records' do
+ let(:name) { 'new_model' }
+ let(:project) { existing_model.project }
+ let(:metadata) { [{ key: 'key1', value: 'value1' }, { key: 'key1', value: 'value2' }] }
+
+ it 'raises an error', :aggregate_failures do
+ expect { create_model }.to raise_error(ActiveRecord::RecordInvalid)
+ end
+ end
+
+ # TODO: Ensure consisted error responses https://gitlab.com/gitlab-org/gitlab/-/issues/429731
+ context 'for metadata with invalid keys, it does not create invalid records' do
+ let(:name) { 'new_model' }
+ let(:project) { existing_model.project }
+ let(:metadata) { [{ key: 'key1', value: 'value1' }, { key: '', value: 'value2' }] }
+
+ it 'raises an error', :aggregate_failures do
+ expect { create_model }.to raise_error(ActiveRecord::RecordInvalid)
+ end
+ end
+ end
+end
diff --git a/spec/services/ml/find_model_service_spec.rb b/spec/services/ml/find_model_service_spec.rb
new file mode 100644
index 00000000000..027298d979a
--- /dev/null
+++ b/spec/services/ml/find_model_service_spec.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe ::Ml::FindModelService, feature_category: :mlops do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:existing_model) { create(:ml_models) }
+ let(:finder) { described_class.new(project, name) }
+
+ describe '#execute' do
+ context 'when model name does not exist in the project' do
+ let(:name) { 'new_model' }
+ let(:project) { existing_model.project }
+
+ it 'reutrns nil' do
+ expect(finder.execute).to be nil
+ end
+ end
+
+ context 'when model with name exists' do
+ let(:name) { existing_model.name }
+ let(:project) { existing_model.project }
+
+ it 'returns the existing model' do
+ expect(finder.execute).to eq(existing_model)
+ end
+ end
+ end
+end
diff --git a/spec/services/ml/find_or_create_model_service_spec.rb b/spec/services/ml/find_or_create_model_service_spec.rb
index 6ddae20f8d6..5d5eaea0a72 100644
--- a/spec/services/ml/find_or_create_model_service_spec.rb
+++ b/spec/services/ml/find_or_create_model_service_spec.rb
@@ -3,10 +3,13 @@
require 'spec_helper'
RSpec.describe ::Ml::FindOrCreateModelService, feature_category: :mlops do
+ let_it_be(:user) { create(:user) }
let_it_be(:existing_model) { create(:ml_models) }
let_it_be(:another_project) { create(:project) }
+ let_it_be(:description) { 'description' }
+ let_it_be(:metadata) { [] }
- subject(:create_model) { described_class.new(project, name).execute }
+ subject(:create_model) { described_class.new(project, name, user, description, metadata).execute }
describe '#execute' do
context 'when model name does not exist in the project' do
diff --git a/spec/services/ml/find_or_create_model_version_service_spec.rb b/spec/services/ml/find_or_create_model_version_service_spec.rb
index 1211a9b1165..e5ca7c3a450 100644
--- a/spec/services/ml/find_or_create_model_version_service_spec.rb
+++ b/spec/services/ml/find_or_create_model_version_service_spec.rb
@@ -5,14 +5,18 @@ require 'spec_helper'
RSpec.describe ::Ml::FindOrCreateModelVersionService, feature_category: :mlops do
let_it_be(:existing_version) { create(:ml_model_versions) }
let_it_be(:another_project) { create(:project) }
+ let_it_be(:user) { create(:user) }
let(:package) { nil }
+ let(:description) { nil }
let(:params) do
{
model_name: name,
version: version,
- package: package
+ package: package,
+ description: description,
+ user: user
}
end
@@ -26,6 +30,7 @@ RSpec.describe ::Ml::FindOrCreateModelVersionService, feature_category: :mlops d
it 'returns existing model version', :aggregate_failures do
expect { model_version }.to change { Ml::ModelVersion.count }.by(0)
+ expect { model_version }.to change { Ml::Candidate.count }.by(0)
expect(model_version).to eq(existing_version)
end
end
@@ -34,15 +39,18 @@ RSpec.describe ::Ml::FindOrCreateModelVersionService, feature_category: :mlops d
let(:project) { existing_version.project }
let(:name) { 'a_new_model' }
let(:version) { '2.0.0' }
+ let(:description) { 'A model version' }
let(:package) { create(:ml_model_package, project: project, name: name, version: version) }
it 'creates a new model version', :aggregate_failures do
- expect { model_version }.to change { Ml::ModelVersion.count }
+ expect { model_version }.to change { Ml::ModelVersion.count }.by(1).and change { Ml::Candidate.count }.by(1)
expect(model_version.name).to eq(name)
expect(model_version.version).to eq(version)
expect(model_version.package).to eq(package)
+ expect(model_version.candidate.model_version_id).to eq(model_version.id)
+ expect(model_version.description).to eq(description)
end
end
end
diff --git a/spec/services/ml/model_versions/get_model_version_service_spec.rb b/spec/services/ml/model_versions/get_model_version_service_spec.rb
new file mode 100644
index 00000000000..b46a0bf6d1d
--- /dev/null
+++ b/spec/services/ml/model_versions/get_model_version_service_spec.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Ml::ModelVersions::GetModelVersionService, feature_category: :mlops do
+ let_it_be(:existing_version) { create(:ml_model_versions) }
+ let_it_be(:another_project) { create(:project) }
+
+ subject(:model_version) { described_class.new(project, name, version).execute }
+
+ describe '#execute' do
+ context 'when model version exists' do
+ let(:name) { existing_version.name }
+ let(:version) { existing_version.version }
+ let(:project) { existing_version.project }
+
+ it { is_expected.to eq(existing_version) }
+ end
+
+ context 'when model version does not exist' do
+ let(:project) { existing_version.project }
+ let(:name) { 'a_new_model' }
+ let(:version) { '2.0.0' }
+
+ it { is_expected.to be_nil }
+ end
+ end
+end
diff --git a/spec/services/ml/update_model_service_spec.rb b/spec/services/ml/update_model_service_spec.rb
new file mode 100644
index 00000000000..35df62559e6
--- /dev/null
+++ b/spec/services/ml/update_model_service_spec.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe ::Ml::UpdateModelService, feature_category: :mlops do
+ let_it_be(:model) { create(:ml_models) }
+ let_it_be(:description) { 'updated model description' }
+ let(:service) { described_class.new(model, description) }
+
+ describe '#execute' do
+ context 'when supplied with a non-model object' do
+ let(:model) { nil }
+
+ it 'returns nil' do
+ expect { service.execute }.to raise_error(NoMethodError)
+ end
+ end
+
+ context 'with an existing model' do
+ it 'updates the description' do
+ updated = service.execute
+ expect(updated.class).to be(Ml::Model)
+ expect(updated.description).to eq(description)
+ end
+ end
+ end
+end
diff --git a/spec/services/notes/create_service_spec.rb b/spec/services/notes/create_service_spec.rb
index 0cc66696184..c1b15ec7681 100644
--- a/spec/services/notes/create_service_spec.rb
+++ b/spec/services/notes/create_service_spec.rb
@@ -3,7 +3,8 @@
require 'spec_helper'
RSpec.describe Notes::CreateService, feature_category: :team_planning do
- let_it_be(:project) { create(:project, :repository) }
+ let_it_be(:group) { create(:group) }
+ let_it_be(:project) { create(:project, :repository, group: group) }
let_it_be(:issue) { create(:issue, project: project) }
let_it_be(:user) { create(:user) }
@@ -13,11 +14,43 @@ RSpec.describe Notes::CreateService, feature_category: :team_planning do
describe '#execute' do
subject(:note) { described_class.new(project, user, opts).execute }
- before do
- project.add_maintainer(user)
+ before_all do
+ group.add_maintainer(user)
end
context "valid params" do
+ context 'when noteable is an issue that belongs directly to a group' do
+ it 'creates a note without a project and correct namespace', :aggregate_failures do
+ group_issue = create(:issue, :group_level, namespace: group)
+ note_params = { note: 'test note', noteable: group_issue }
+
+ expect do
+ described_class.new(nil, user, note_params).execute
+ end.to change { Note.count }.by(1)
+
+ created_note = Note.last
+
+ expect(created_note.namespace).to eq(group)
+ expect(created_note.project).to be_nil
+ end
+ end
+
+ context 'when noteable is a work item that belongs directly to a group' do
+ it 'creates a note without a project and correct namespace', :aggregate_failures do
+ group_work_item = create(:work_item, :group_level, namespace: group)
+ note_params = { note: 'test note', noteable: group_work_item }
+
+ expect do
+ described_class.new(nil, user, note_params).execute
+ end.to change { Note.count }.by(1)
+
+ created_note = Note.last
+
+ expect(created_note.namespace).to eq(group)
+ expect(created_note.project).to be_nil
+ end
+ end
+
it_behaves_like 'does not trigger GraphQL subscription mergeRequestMergeStatusUpdated' do
let(:action) { note }
end
@@ -195,21 +228,35 @@ RSpec.describe Notes::CreateService, feature_category: :team_planning do
let(:new_opts) { opts.merge(noteable_type: 'MergeRequest', noteable_id: merge_request.id) }
- it 'calls MergeRequests::MarkReviewerReviewedService service' do
- expect_next_instance_of(
- MergeRequests::MarkReviewerReviewedService,
- project: project_with_repo, current_user: user
- ) do |service|
- expect(service).to receive(:execute).with(merge_request)
+ context 'when mr_request_changes feature flag is disabled' do
+ before do
+ stub_feature_flags(mr_request_changes: false)
end
- described_class.new(project_with_repo, user, new_opts).execute
+ it 'calls MergeRequests::UpdateReviewerStateService service' do
+ expect_next_instance_of(
+ MergeRequests::UpdateReviewerStateService,
+ project: project_with_repo, current_user: user
+ ) do |service|
+ expect(service).to receive(:execute).with(merge_request, "reviewed")
+ end
+
+ described_class.new(project_with_repo, user, new_opts).execute
+ end
+
+ it 'does not call MergeRequests::UpdateReviewerStateService service when skip_set_reviewed is true' do
+ expect(MergeRequests::UpdateReviewerStateService).not_to receive(:new)
+
+ described_class.new(project_with_repo, user, new_opts).execute(skip_set_reviewed: true)
+ end
end
- it 'does not call MergeRequests::MarkReviewerReviewedService service when skip_set_reviewed is true' do
- expect(MergeRequests::MarkReviewerReviewedService).not_to receive(:new)
+ context 'when mr_request_changes feature flag is enabled' do
+ it 'does not call MergeRequests::UpdateReviewerStateService service when skip_set_reviewed is true' do
+ expect(MergeRequests::UpdateReviewerStateService).not_to receive(:new)
- described_class.new(project_with_repo, user, new_opts).execute(skip_set_reviewed: true)
+ described_class.new(project_with_repo, user, new_opts).execute(skip_set_reviewed: true)
+ end
end
context 'noteable highlight cache clearing' do
diff --git a/spec/services/organizations/create_service_spec.rb b/spec/services/organizations/create_service_spec.rb
new file mode 100644
index 00000000000..7d9bf64ddd3
--- /dev/null
+++ b/spec/services/organizations/create_service_spec.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Organizations::CreateService, feature_category: :cell do
+ describe '#execute' do
+ let_it_be(:user) { create(:user) }
+
+ let(:current_user) { user }
+ let(:params) { attributes_for(:organization) }
+
+ subject(:response) { described_class.new(current_user: current_user, params: params).execute }
+
+ context 'when user does not have permission' do
+ let(:current_user) { nil }
+
+ it 'returns an error' do
+ expect(response).to be_error
+
+ expect(response.message).to match_array(
+ ['You have insufficient permissions to create organizations'])
+ end
+ end
+
+ context 'when user has permission' do
+ it 'creates an organization' do
+ expect { response }.to change { Organizations::Organization.count }
+
+ expect(response).to be_success
+ end
+
+ it 'returns an error when the organization is not persisted' do
+ params[:name] = nil
+
+ expect(response).to be_error
+ expect(response.message).to match_array(["Name can't be blank"])
+ end
+ end
+ end
+end
diff --git a/spec/services/packages/create_dependency_service_spec.rb b/spec/services/packages/create_dependency_service_spec.rb
index 06a7a13bdd9..c50bf988899 100644
--- a/spec/services/packages/create_dependency_service_spec.rb
+++ b/spec/services/packages/create_dependency_service_spec.rb
@@ -33,8 +33,8 @@ RSpec.describe Packages::CreateDependencyService, feature_category: :package_reg
expect { subject }
.to change { Packages::Dependency.count }.by(1)
.and change { Packages::DependencyLink.count }.by(1)
- expect(dependency_names).to match_array(%w(express))
- expect(dependency_link_types).to match_array(%w(dependencies))
+ expect(dependency_names).to match_array(%w[express])
+ expect(dependency_link_types).to match_array(%w[dependencies])
end
context 'with repeated packages' do
@@ -49,8 +49,8 @@ RSpec.describe Packages::CreateDependencyService, feature_category: :package_reg
expect { subject }
.to change { Packages::Dependency.count }.by(4)
.and change { Packages::DependencyLink.count }.by(6)
- expect(dependency_names).to match_array(%w(d3 d3 d3 dagre-d3 dagre-d3 express))
- expect(dependency_link_types).to match_array(%w(bundleDependencies dependencies dependencies devDependencies devDependencies peerDependencies))
+ expect(dependency_names).to match_array(%w[d3 d3 d3 dagre-d3 dagre-d3 express])
+ expect(dependency_link_types).to match_array(%w[bundleDependencies dependencies dependencies devDependencies devDependencies peerDependencies])
end
end
@@ -72,8 +72,8 @@ RSpec.describe Packages::CreateDependencyService, feature_category: :package_reg
expect { subject }
.to change { Packages::Dependency.count }.by(1)
.and change { Packages::DependencyLink.count }.by(1)
- expect(dependency_names).to match_array(%w(express))
- expect(dependency_link_types).to match_array(%w(dependencies))
+ expect(dependency_names).to match_array(%w[express])
+ expect(dependency_link_types).to match_array(%w[dependencies])
end
end
@@ -105,8 +105,8 @@ RSpec.describe Packages::CreateDependencyService, feature_category: :package_reg
expect { subject }
.to change { Packages::Dependency.count }.by(1)
.and change { Packages::DependencyLink.count }.by(1)
- expect(dependency_names).to match_array(%w(express))
- expect(dependency_link_types).to match_array(%w(dependencies))
+ expect(dependency_names).to match_array(%w[express])
+ expect(dependency_link_types).to match_array(%w[dependencies])
end
end
end
diff --git a/spec/services/packages/npm/create_package_service_spec.rb b/spec/services/packages/npm/create_package_service_spec.rb
index 1c935c27d7f..867dc582771 100644
--- a/spec/services/packages/npm/create_package_service_spec.rb
+++ b/spec/services/packages/npm/create_package_service_spec.rb
@@ -2,177 +2,179 @@
require 'spec_helper'
RSpec.describe Packages::Npm::CreatePackageService, feature_category: :package_registry do
- include ExclusiveLeaseHelpers
-
- let(:namespace) { create(:namespace) }
- let(:project) { create(:project, namespace: namespace) }
- let(:user) { project.owner }
- let(:version) { '1.0.1' }
-
- let(:params) do
- Gitlab::Json.parse(fixture_file('packages/npm/payload.json')
- .gsub('@root/npm-test', package_name)
- .gsub('1.0.1', version)).with_indifferent_access
- end
-
- let(:package_name) { "@#{namespace.path}/my-app" }
- let(:version_data) { params.dig('versions', version) }
- let(:lease_key) { "packages:npm:create_package_service:packages:#{project.id}_#{package_name}_#{version}" }
let(:service) { described_class.new(project, user, params) }
subject { service.execute }
- shared_examples 'valid package' do
- it 'creates a package' do
- expect { subject }
- .to change { Packages::Package.count }.by(1)
- .and change { Packages::Package.npm.count }.by(1)
- .and change { Packages::Tag.count }.by(1)
- .and change { Packages::Npm::Metadatum.count }.by(1)
- end
-
- it_behaves_like 'assigns the package creator' do
- let(:package) { subject }
- end
+ describe '#execute' do
+ include ExclusiveLeaseHelpers
- it { is_expected.to be_valid }
+ let_it_be(:namespace) { create(:namespace) }
+ let_it_be_with_reload(:project) { create(:project, namespace: namespace) }
+ let_it_be(:user) { project.owner }
- it 'creates a package with name and version' do
- package = subject
+ let(:version) { '1.0.1' }
- expect(package.name).to eq(package_name)
- expect(package.version).to eq(version)
+ let(:params) do
+ Gitlab::Json.parse(fixture_file('packages/npm/payload.json')
+ .gsub('@root/npm-test', package_name)
+ .gsub('1.0.1', version)).with_indifferent_access
end
- it { expect(subject.npm_metadatum.package_json).to eq(version_data) }
+ let(:package_name) { "@#{namespace.path}/my-app" }
+ let(:version_data) { params.dig('versions', version) }
+ let(:lease_key) { "packages:npm:create_package_service:packages:#{project.id}_#{package_name}_#{version}" }
+
+ shared_examples 'valid package' do
+ it 'creates a package' do
+ expect { subject }
+ .to change { Packages::Package.count }.by(1)
+ .and change { Packages::Package.npm.count }.by(1)
+ .and change { Packages::Tag.count }.by(1)
+ .and change { Packages::Npm::Metadatum.count }.by(1)
+ end
- it { expect(subject.name).to eq(package_name) }
- it { expect(subject.version).to eq(version) }
+ it_behaves_like 'assigns the package creator' do
+ let(:package) { subject }
+ end
- context 'with build info' do
- let(:job) { create(:ci_build, user: user) }
- let(:params) { super().merge(build: job) }
+ it { is_expected.to be_valid }
- it_behaves_like 'assigns build to package'
- it_behaves_like 'assigns status to package'
+ it 'creates a package with name and version' do
+ package = subject
- it 'creates a package file build info' do
- expect { subject }.to change { Packages::PackageFileBuildInfo.count }.by(1)
+ expect(package.name).to eq(package_name)
+ expect(package.version).to eq(version)
end
- end
- context 'when the npm metadatum creation results in a size error' do
- shared_examples 'a package json structure size too large error' do
- it 'does not create the package' do
- expect(Gitlab::ErrorTracking).to receive(:track_exception).with(
- instance_of(ActiveRecord::RecordInvalid),
- field_sizes: expected_field_sizes
- )
+ it { expect(subject.npm_metadatum.package_json).to eq(version_data) }
- expect { subject }.to raise_error(ActiveRecord::RecordInvalid, /structure is too large/)
- .and not_change { Packages::Package.count }
- .and not_change { Packages::Package.npm.count }
- .and not_change { Packages::Tag.count }
- .and not_change { Packages::Npm::Metadatum.count }
- end
- end
+ it { expect(subject.name).to eq(package_name) }
+ it { expect(subject.version).to eq(version) }
- context 'when some of the field sizes are above the error tracking size' do
- let(:package_json) do
- params[:versions][version].except(*::Packages::Npm::CreatePackageService::PACKAGE_JSON_NOT_ALLOWED_FIELDS)
- end
+ context 'with build info' do
+ let_it_be(:job) { create(:ci_build, user: user) }
+ let(:params) { super().merge(build: job) }
- # Only the fields that exceed the field size limit should be passed to error tracking
- let(:expected_field_sizes) do
- {
- 'test' => ('test' * 10000).size,
- 'field2' => ('a' * (::Packages::Npm::Metadatum::MIN_PACKAGE_JSON_FIELD_SIZE_FOR_ERROR_TRACKING + 1)).size
- }
- end
+ it_behaves_like 'assigns build to package'
+ it_behaves_like 'assigns status to package'
- before do
- params[:versions][version][:test] = 'test' * 10000
- params[:versions][version][:field1] =
- 'a' * (::Packages::Npm::Metadatum::MIN_PACKAGE_JSON_FIELD_SIZE_FOR_ERROR_TRACKING - 1)
- params[:versions][version][:field2] =
- 'a' * (::Packages::Npm::Metadatum::MIN_PACKAGE_JSON_FIELD_SIZE_FOR_ERROR_TRACKING + 1)
+ it 'creates a package file build info' do
+ expect { subject }.to change { Packages::PackageFileBuildInfo.count }.by(1)
end
-
- it_behaves_like 'a package json structure size too large error'
end
- context 'when all of the field sizes are below the error tracking size' do
- let(:package_json) do
- params[:versions][version].except(*::Packages::Npm::CreatePackageService::PACKAGE_JSON_NOT_ALLOWED_FIELDS)
+ context 'when the npm metadatum creation results in a size error' do
+ shared_examples 'a package json structure size too large error' do
+ it 'does not create the package' do
+ expect(Gitlab::ErrorTracking).to receive(:track_exception).with(
+ instance_of(ActiveRecord::RecordInvalid),
+ field_sizes: expected_field_sizes
+ )
+
+ expect { subject }.to raise_error(ActiveRecord::RecordInvalid, /structure is too large/)
+ .and not_change { Packages::Package.count }
+ .and not_change { Packages::Package.npm.count }
+ .and not_change { Packages::Tag.count }
+ .and not_change { Packages::Npm::Metadatum.count }
+ end
end
- let(:expected_size) { ('a' * (::Packages::Npm::Metadatum::MIN_PACKAGE_JSON_FIELD_SIZE_FOR_ERROR_TRACKING - 1)).size }
- # Only the five largest fields should be passed to error tracking
- let(:expected_field_sizes) do
- {
- 'field1' => expected_size,
- 'field2' => expected_size,
- 'field3' => expected_size,
- 'field4' => expected_size,
- 'field5' => expected_size
- }
- end
+ context 'when some of the field sizes are above the error tracking size' do
+ let(:package_json) do
+ params[:versions][version].except(*::Packages::Npm::CreatePackageService::PACKAGE_JSON_NOT_ALLOWED_FIELDS)
+ end
- before do
- 5.times do |i|
- params[:versions][version]["field#{i + 1}"] =
+ # Only the fields that exceed the field size limit should be passed to error tracking
+ let(:expected_field_sizes) do
+ {
+ 'test' => ('test' * 10000).size,
+ 'field2' => ('a' * (::Packages::Npm::Metadatum::MIN_PACKAGE_JSON_FIELD_SIZE_FOR_ERROR_TRACKING + 1)).size
+ }
+ end
+
+ before do
+ params[:versions][version][:test] = 'test' * 10000
+ params[:versions][version][:field1] =
'a' * (::Packages::Npm::Metadatum::MIN_PACKAGE_JSON_FIELD_SIZE_FOR_ERROR_TRACKING - 1)
+ params[:versions][version][:field2] =
+ 'a' * (::Packages::Npm::Metadatum::MIN_PACKAGE_JSON_FIELD_SIZE_FOR_ERROR_TRACKING + 1)
end
+
+ it_behaves_like 'a package json structure size too large error'
end
- it_behaves_like 'a package json structure size too large error'
- end
- end
+ context 'when all of the field sizes are below the error tracking size' do
+ let(:package_json) do
+ params[:versions][version].except(*::Packages::Npm::CreatePackageService::PACKAGE_JSON_NOT_ALLOWED_FIELDS)
+ end
- context 'when the npm metadatum creation results in a different error' do
- it 'does not track the error' do
- error_message = 'boom'
- invalid_npm_metadatum_error = ActiveRecord::RecordInvalid.new(
- build(:npm_metadatum).tap do |metadatum|
- metadatum.errors.add(:base, error_message)
+ let(:expected_size) { ('a' * (::Packages::Npm::Metadatum::MIN_PACKAGE_JSON_FIELD_SIZE_FOR_ERROR_TRACKING - 1)).size }
+ # Only the five largest fields should be passed to error tracking
+ let(:expected_field_sizes) do
+ {
+ 'field1' => expected_size,
+ 'field2' => expected_size,
+ 'field3' => expected_size,
+ 'field4' => expected_size,
+ 'field5' => expected_size
+ }
end
- )
- allow_next_instance_of(::Packages::Package) do |package|
- allow(package).to receive(:create_npm_metadatum!).and_raise(invalid_npm_metadatum_error)
+ before do
+ 5.times do |i|
+ params[:versions][version]["field#{i + 1}"] =
+ 'a' * (::Packages::Npm::Metadatum::MIN_PACKAGE_JSON_FIELD_SIZE_FOR_ERROR_TRACKING - 1)
+ end
+ end
+
+ it_behaves_like 'a package json structure size too large error'
end
+ end
- expect(Gitlab::ErrorTracking).not_to receive(:track_exception)
+ context 'when the npm metadatum creation results in a different error' do
+ it 'does not track the error' do
+ error_message = 'boom'
+ invalid_npm_metadatum_error = ActiveRecord::RecordInvalid.new(
+ build(:npm_metadatum).tap do |metadatum|
+ metadatum.errors.add(:base, error_message)
+ end
+ )
- expect { subject }.to raise_error(ActiveRecord::RecordInvalid, /#{error_message}/)
- end
- end
+ allow_next_instance_of(::Packages::Package) do |package|
+ allow(package).to receive(:create_npm_metadatum!).and_raise(invalid_npm_metadatum_error)
+ end
- described_class::PACKAGE_JSON_NOT_ALLOWED_FIELDS.each do |field|
- context "with not allowed #{field} field" do
- before do
- params[:versions][version][field] = 'test'
+ expect(Gitlab::ErrorTracking).not_to receive(:track_exception)
+
+ expect { subject }.to raise_error(ActiveRecord::RecordInvalid, /#{error_message}/)
end
+ end
- it 'is persisted without the field' do
- expect { subject }
- .to change { Packages::Package.count }.by(1)
- .and change { Packages::Package.npm.count }.by(1)
- .and change { Packages::Tag.count }.by(1)
- .and change { Packages::Npm::Metadatum.count }.by(1)
- expect(subject.npm_metadatum.package_json[field]).to be_blank
+ described_class::PACKAGE_JSON_NOT_ALLOWED_FIELDS.each do |field|
+ context "with not allowed #{field} field" do
+ before do
+ params[:versions][version][field] = 'test'
+ end
+
+ it 'is persisted without the field' do
+ expect { subject }
+ .to change { Packages::Package.count }.by(1)
+ .and change { Packages::Package.npm.count }.by(1)
+ .and change { Packages::Tag.count }.by(1)
+ .and change { Packages::Npm::Metadatum.count }.by(1)
+ expect(subject.npm_metadatum.package_json[field]).to be_blank
+ end
end
end
end
- end
- describe '#execute' do
context 'scoped package' do
it_behaves_like 'valid package'
end
context 'when user is no project member' do
- let(:user) { create(:user) }
+ let_it_be(:user) { create(:user) }
it_behaves_like 'valid package'
end
@@ -320,13 +322,111 @@ RSpec.describe Packages::Npm::CreatePackageService, feature_category: :package_r
it { expect(subject[:message]).to eq 'Could not obtain package lease. Please try again.' }
end
- context 'when many of the same packages are created at the same time', :delete do
- it 'only creates one package' do
- expect { create_packages(project, user, params) }.to change { Packages::Package.count }.by(1)
+ context 'when feature flag :packages_package_protection is disabled' do
+ let_it_be_with_reload(:package_protection_rule) { create(:package_protection_rule, package_type: :npm, project: project) }
+
+ before do
+ stub_feature_flags(packages_protected_packages: false)
+ end
+
+ context 'with matching package protection rule for all roles' do
+ using RSpec::Parameterized::TableSyntax
+
+ let(:package_name_pattern_no_match) { "#{package_name}_no_match" }
+
+ where(:package_name_pattern, :push_protected_up_to_access_level) do
+ ref(:package_name) | :developer
+ ref(:package_name) | :owner
+ ref(:package_name_pattern_no_match) | :developer
+ ref(:package_name_pattern_no_match) | :owner
+ end
+
+ with_them do
+ before do
+ package_protection_rule.update!(package_name_pattern: package_name_pattern, push_protected_up_to_access_level: push_protected_up_to_access_level)
+ end
+
+ it_behaves_like 'valid package'
+ end
+ end
+ end
+
+ context 'with package protection rule for different roles and package_name_patterns' do
+ using RSpec::Parameterized::TableSyntax
+
+ let_it_be_with_reload(:package_protection_rule) { create(:package_protection_rule, package_type: :npm, project: project) }
+ let_it_be(:project_developer) { create(:user).tap { |u| project.add_developer(u) } }
+ let_it_be(:project_maintainer) { create(:user).tap { |u| project.add_maintainer(u) } }
+
+ let(:project_owner) { project.owner }
+ let(:package_name_pattern_no_match) { "#{package_name}_no_match" }
+
+ let(:service) { described_class.new(project, current_user, params) }
+
+ shared_examples 'protected package' do
+ it { is_expected.to include http_status: 403, message: 'Package protected.' }
+
+ it 'does not create any npm-related package records' do
+ expect { subject }
+ .to not_change { Packages::Package.count }
+ .and not_change { Packages::Package.npm.count }
+ .and not_change { Packages::Tag.count }
+ .and not_change { Packages::Npm::Metadatum.count }
+ end
+ end
+
+ where(:package_name_pattern, :push_protected_up_to_access_level, :current_user, :shared_examples_name) do
+ ref(:package_name) | :developer | ref(:project_developer) | 'protected package'
+ ref(:package_name) | :developer | ref(:project_owner) | 'valid package'
+ ref(:package_name) | :maintainer | ref(:project_maintainer) | 'protected package'
+ ref(:package_name) | :owner | ref(:project_owner) | 'protected package'
+ ref(:package_name_pattern_no_match) | :developer | ref(:project_owner) | 'valid package'
+ ref(:package_name_pattern_no_match) | :owner | ref(:project_owner) | 'valid package'
+ end
+
+ with_them do
+ before do
+ package_protection_rule.update!(package_name_pattern: package_name_pattern, push_protected_up_to_access_level: push_protected_up_to_access_level)
+ end
+
+ it_behaves_like params[:shared_examples_name]
+ end
+ end
+
+ describe '#lease_key' do
+ subject { service.send(:lease_key) }
+
+ it 'returns an unique key' do
+ is_expected.to eq lease_key
end
end
+ end
- context 'when many packages with different versions are created at the same time', :delete do
+ context 'when many of the same packages are created at the same time', :delete do
+ let(:namespace) { create(:namespace) }
+ let(:project) { create(:project, namespace: namespace) }
+ let(:user) { project.owner }
+
+ let(:version) { '1.0.1' }
+
+ let(:params) do
+ Gitlab::Json.parse(
+ fixture_file('packages/npm/payload.json')
+ .gsub('@root/npm-test', package_name)
+ .gsub('1.0.1', version)
+ ).with_indifferent_access
+ end
+
+ let(:package_name) { "@#{namespace.path}/my-app" }
+ let(:service) { described_class.new(project, user, params) }
+
+ subject { service.execute }
+
+ it 'only creates one package' do
+ expect { create_packages(project, user, params) }.to change { Packages::Package.count }.by(1)
+ end
+
+ context 'with different versions' do
it 'creates all packages' do
expect { create_packages_with_versions(project, user, params) }.to change { Packages::Package.count }.by(5)
end
@@ -367,12 +467,4 @@ RSpec.describe Packages::Npm::CreatePackageService, feature_category: :package_r
threads.each(&:join)
end
end
-
- describe '#lease_key' do
- subject { service.send(:lease_key) }
-
- it 'returns an unique key' do
- is_expected.to eq lease_key
- end
- end
end
diff --git a/spec/services/packages/npm/generate_metadata_service_spec.rb b/spec/services/packages/npm/generate_metadata_service_spec.rb
index d8e794405e6..f5d7f13d22c 100644
--- a/spec/services/packages/npm/generate_metadata_service_spec.rb
+++ b/spec/services/packages/npm/generate_metadata_service_spec.rb
@@ -44,7 +44,7 @@ RSpec.describe ::Packages::Npm::GenerateMetadataService, feature_category: :pack
with_them do
if params[:has_dependencies]
- ::Packages::DependencyLink.dependency_types.each_key do |dependency_type| # rubocop:disable RSpec/UselessDynamicDefinition
+ ::Packages::DependencyLink.dependency_types.each_key do |dependency_type| # rubocop:disable RSpec/UselessDynamicDefinition -- `dependency_type` used in `let_it_be`
let_it_be("package_dependency_link_for_#{dependency_type}") do
create(:packages_dependency_link, package: package1, dependency_type: dependency_type)
end
diff --git a/spec/services/packages/nuget/check_duplicates_service_spec.rb b/spec/services/packages/nuget/check_duplicates_service_spec.rb
index 9675aa5f5e2..6274036800a 100644
--- a/spec/services/packages/nuget/check_duplicates_service_spec.rb
+++ b/spec/services/packages/nuget/check_duplicates_service_spec.rb
@@ -3,8 +3,6 @@
require 'spec_helper'
RSpec.describe Packages::Nuget::CheckDuplicatesService, feature_category: :package_registry do
- include PackagesManagerApiSpecHelpers
-
let_it_be(:project) { create(:project) }
let_it_be(:user) { create(:user) }
let_it_be(:file_name) { 'package.nupkg' }
@@ -12,7 +10,7 @@ RSpec.describe Packages::Nuget::CheckDuplicatesService, feature_category: :packa
let(:params) do
{
file_name: file_name,
- file: temp_file(file_name)
+ file: File.open(expand_fixture_path('packages/nuget/package.nupkg'))
}
end
diff --git a/spec/services/packages/nuget/create_dependency_service_spec.rb b/spec/services/packages/nuget/create_dependency_service_spec.rb
index 10daec8b871..7e14779cb92 100644
--- a/spec/services/packages/nuget/create_dependency_service_spec.rb
+++ b/spec/services/packages/nuget/create_dependency_service_spec.rb
@@ -41,12 +41,12 @@ RSpec.describe Packages::Nuget::CreateDependencyService, feature_category: :pack
subject { service.execute }
- it_behaves_like 'creating dependencies, links and nuget metadata for', %w(Castle.Core Moqi Newtonsoft.Json Test.Dependency), 4, 4
+ it_behaves_like 'creating dependencies, links and nuget metadata for', %w[Castle.Core Moqi Newtonsoft.Json Test.Dependency], 4, 4
context 'with existing dependencies' do
let_it_be(:exisiting_dependency) { create(:packages_dependency, name: 'Moqi', version_pattern: '2.5.6') }
- it_behaves_like 'creating dependencies, links and nuget metadata for', %w(Castle.Core Moqi Newtonsoft.Json Test.Dependency), 3, 4
+ it_behaves_like 'creating dependencies, links and nuget metadata for', %w[Castle.Core Moqi Newtonsoft.Json Test.Dependency], 3, 4
end
context 'with dependencies with no target framework' do
@@ -59,7 +59,7 @@ RSpec.describe Packages::Nuget::CreateDependencyService, feature_category: :pack
]
end
- it_behaves_like 'creating dependencies, links and nuget metadata for', %w(Castle.Core Moqi Newtonsoft.Json Test.Dependency), 4, 4
+ it_behaves_like 'creating dependencies, links and nuget metadata for', %w[Castle.Core Moqi Newtonsoft.Json Test.Dependency], 4, 4
end
context 'with empty dependencies' do
diff --git a/spec/services/packages/nuget/extract_metadata_file_service_spec.rb b/spec/services/packages/nuget/extract_metadata_file_service_spec.rb
index 4c761826b53..ac5749c2dc4 100644
--- a/spec/services/packages/nuget/extract_metadata_file_service_spec.rb
+++ b/spec/services/packages/nuget/extract_metadata_file_service_spec.rb
@@ -3,6 +3,8 @@
require 'spec_helper'
RSpec.describe Packages::Nuget::ExtractMetadataFileService, feature_category: :package_registry do
+ include PackagesManagerApiSpecHelpers
+
let_it_be(:package_file) { build(:package_file, :nuget) }
let_it_be(:package_zip_file) { Zip::File.new(package_file.file) }
@@ -38,6 +40,18 @@ RSpec.describe Packages::Nuget::ExtractMetadataFileService, feature_category: :p
it 'returns the nuspec file content' do
expect(subject.payload.squish).to include(expected_metadata)
end
+
+ context 'with InputStream zip' do
+ let(:package_zip_file) do
+ Zip::InputStream.open(
+ temp_file('package.nupkg', content: File.open(package_file.file.path))
+ )
+ end
+
+ it 'returns the nuspec file content' do
+ expect(subject.payload.squish).to include(expected_metadata)
+ end
+ end
end
context 'without the nuspec file' do
diff --git a/spec/services/packages/nuget/metadata_extraction_service_spec.rb b/spec/services/packages/nuget/metadata_extraction_service_spec.rb
index 81a4e4a430b..46d5449d52b 100644
--- a/spec/services/packages/nuget/metadata_extraction_service_spec.rb
+++ b/spec/services/packages/nuget/metadata_extraction_service_spec.rb
@@ -4,7 +4,8 @@ require 'spec_helper'
RSpec.describe Packages::Nuget::MetadataExtractionService, feature_category: :package_registry do
let_it_be(:package_file) { build(:package_file, :nuget) }
- let(:service) { described_class.new(package_file) }
+ let(:package_zip_file) { Zip::File.new(package_file.file) }
+ let(:service) { described_class.new(package_zip_file) }
describe '#execute' do
subject { service.execute }
@@ -50,8 +51,8 @@ RSpec.describe Packages::Nuget::MetadataExtractionService, feature_category: :pa
end
it 'calls the necessary services and executes the metadata extraction' do
- expect_next_instance_of(Packages::Nuget::ProcessPackageFileService, package_file) do |service|
- expect(service).to receive(:execute).and_return(ServiceResponse.success(payload: { nuspec_file_content: nuspec_file_content }))
+ expect_next_instance_of(Packages::Nuget::ExtractMetadataFileService, package_zip_file) do |service|
+ expect(service).to receive(:execute).and_return(ServiceResponse.success(payload: nuspec_file_content))
end
expect_next_instance_of(Packages::Nuget::ExtractMetadataContentService, nuspec_file_content) do |service|
diff --git a/spec/services/packages/nuget/process_package_file_service_spec.rb b/spec/services/packages/nuget/process_package_file_service_spec.rb
index cdeb5b32737..70e8bcb8c5c 100644
--- a/spec/services/packages/nuget/process_package_file_service_spec.rb
+++ b/spec/services/packages/nuget/process_package_file_service_spec.rb
@@ -14,25 +14,14 @@ RSpec.describe Packages::Nuget::ProcessPackageFileService, feature_category: :pa
it { expect { subject }.to raise_error(described_class::ExtractionError, error_message) }
end
- shared_examples 'not creating a symbol file' do
- it 'does not call the CreateSymbolFilesService' do
- expect(Packages::Nuget::Symbols::CreateSymbolFilesService).not_to receive(:new)
-
- expect(subject).to be_success
- end
- end
-
context 'with valid package file' do
- it 'calls the ExtractMetadataFileService' do
- expect_next_instance_of(Packages::Nuget::ExtractMetadataFileService, instance_of(Zip::File)) do |service|
- expect(service).to receive(:execute) do
- instance_double(ServiceResponse).tap do |response|
- expect(response).to receive(:payload).and_return(instance_of(String))
- end
- end
+ it 'calls the UpdatePackageFromMetadataService' do
+ expect_next_instance_of(Packages::Nuget::UpdatePackageFromMetadataService, package_file,
+ instance_of(Zip::File)) do |service|
+ expect(service).to receive(:execute)
end
- expect(subject).to be_success
+ subject
end
end
@@ -59,25 +48,5 @@ RSpec.describe Packages::Nuget::ProcessPackageFileService, feature_category: :pa
it_behaves_like 'raises an error', 'invalid package file'
end
-
- context 'with a symbol package file' do
- let(:package_file) { build(:package_file, :snupkg) }
-
- it 'calls the CreateSymbolFilesService' do
- expect_next_instance_of(
- Packages::Nuget::Symbols::CreateSymbolFilesService, package_file.package, instance_of(Zip::File)
- ) do |service|
- expect(service).to receive(:execute)
- end
-
- expect(subject).to be_success
- end
- end
-
- context 'with a non symbol package file' do
- let(:package_file) { build(:package_file, :nuget) }
-
- it_behaves_like 'not creating a symbol file'
- end
end
end
diff --git a/spec/services/packages/nuget/symbols/create_symbol_files_service_spec.rb b/spec/services/packages/nuget/symbols/create_symbol_files_service_spec.rb
index 97bfc3e06a8..96cc46884af 100644
--- a/spec/services/packages/nuget/symbols/create_symbol_files_service_spec.rb
+++ b/spec/services/packages/nuget/symbols/create_symbol_files_service_spec.rb
@@ -15,9 +15,9 @@ RSpec.describe Packages::Nuget::Symbols::CreateSymbolFilesService, feature_categ
describe '#execute' do
subject { service.execute }
- shared_examples 'logs an error' do |error_class|
- it 'logs an error' do
- expect(Gitlab::ErrorTracking).to receive(:log_exception).with(
+ shared_examples 'logging an error' do |error_class|
+ it 'logs the error' do
+ expect(Gitlab::ErrorTracking).to receive(:track_exception).with(
an_instance_of(error_class),
class: described_class.name,
package_id: package.id
@@ -29,8 +29,8 @@ RSpec.describe Packages::Nuget::Symbols::CreateSymbolFilesService, feature_categ
context 'when symbol files are found' do
it 'creates a symbol record and extracts the signature' do
- expect_next_instance_of(Packages::Nuget::Symbols::ExtractSymbolSignatureService,
- instance_of(String)) do |service|
+ expect_next_instance_of(Packages::Nuget::Symbols::ExtractSignatureAndChecksumService,
+ instance_of(File)) do |service|
expect(service).to receive(:execute).and_call_original
end
@@ -47,13 +47,13 @@ RSpec.describe Packages::Nuget::Symbols::CreateSymbolFilesService, feature_categ
expect { subject }.not_to change { package.nuget_symbols.count }
end
- it_behaves_like 'logs an error', described_class::ExtractionError
+ it_behaves_like 'logging an error', described_class::ExtractionError
end
- context 'when creating a symbol record without a signature' do
+ context 'without a signature' do
before do
- allow_next_instance_of(Packages::Nuget::Symbols::ExtractSymbolSignatureService) do |instance|
- allow(instance).to receive(:execute).and_return(ServiceResponse.success(payload: nil))
+ allow_next_instance_of(Packages::Nuget::Symbols::ExtractSignatureAndChecksumService) do |instance|
+ allow(instance).to receive(:execute).and_return(ServiceResponse.success(payload: { signature: nil }))
end
end
@@ -64,12 +64,28 @@ RSpec.describe Packages::Nuget::Symbols::CreateSymbolFilesService, feature_categ
end
end
- context 'when creating duplicate symbol records' do
+ context 'without a checksum' do
+ before do
+ allow_next_instance_of(Packages::Nuget::Symbols::ExtractSignatureAndChecksumService) do |instance|
+ allow(instance).to receive(:execute).and_return(ServiceResponse.success(payload: { checksum: nil }))
+ end
+ end
+
+ it 'does not call create! on the symbol record' do
+ expect(::Packages::Nuget::Symbol).not_to receive(:create!)
+
+ subject
+ end
+ end
+
+ context 'with existing duplicate symbol records' do
let_it_be(:symbol) { create(:nuget_symbol, package: package) }
before do
- allow_next_instance_of(Packages::Nuget::Symbols::ExtractSymbolSignatureService) do |instance|
- allow(instance).to receive(:execute).and_return(ServiceResponse.success(payload: symbol.signature))
+ allow_next_instance_of(Packages::Nuget::Symbols::ExtractSignatureAndChecksumService) do |instance|
+ allow(instance).to receive(:execute).and_return(
+ ServiceResponse.success(payload: { signature: symbol.signature, checksum: symbol.file_sha256 })
+ )
end
end
@@ -77,7 +93,7 @@ RSpec.describe Packages::Nuget::Symbols::CreateSymbolFilesService, feature_categ
expect { subject }.not_to change { package.nuget_symbols.count }
end
- it_behaves_like 'logs an error', ActiveRecord::RecordInvalid
+ it_behaves_like 'logging an error', ActiveRecord::RecordInvalid
end
context 'when a symbol file has the wrong entry size' do
@@ -87,7 +103,7 @@ RSpec.describe Packages::Nuget::Symbols::CreateSymbolFilesService, feature_categ
end
end
- it_behaves_like 'logs an error', described_class::ExtractionError
+ it_behaves_like 'logging an error', described_class::ExtractionError
end
context 'when a symbol file has the wrong entry name' do
@@ -97,7 +113,7 @@ RSpec.describe Packages::Nuget::Symbols::CreateSymbolFilesService, feature_categ
end
end
- it_behaves_like 'logs an error', described_class::ExtractionError
+ it_behaves_like 'logging an error', described_class::ExtractionError
end
end
end
diff --git a/spec/services/packages/nuget/symbols/extract_signature_and_checksum_service_spec.rb b/spec/services/packages/nuget/symbols/extract_signature_and_checksum_service_spec.rb
new file mode 100644
index 00000000000..210b92dfce5
--- /dev/null
+++ b/spec/services/packages/nuget/symbols/extract_signature_and_checksum_service_spec.rb
@@ -0,0 +1,46 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Packages::Nuget::Symbols::ExtractSignatureAndChecksumService, feature_category: :package_registry do
+ let_it_be(:symbol_file_path) { expand_fixture_path('packages/nuget/symbol/package.pdb') }
+ let(:symbol_file) { File.new(symbol_file_path) }
+
+ let(:service) { described_class.new(symbol_file) }
+
+ after do
+ symbol_file.close
+ end
+
+ describe '#execute' do
+ subject { service.execute }
+
+ context 'with a valid symbol file' do
+ it 'returns the signature and checksum' do
+ payload = subject.payload
+
+ expect(payload[:signature]).to eq('b91a152048fc4b3883bf3cf73fbc03f1FFFFFFFF')
+ expect(payload[:checksum]).to eq('20151ab9fc48384b83bf3cf73fbc03f1d49166cc356139845f290d1d315256c0')
+ end
+
+ it 'reads the file in chunks' do
+ expect(symbol_file).to receive(:read).with(described_class::GUID_CHUNK_SIZE).and_call_original
+ expect(symbol_file).to receive(:read).with(described_class::SHA_CHUNK_SIZE, instance_of(String))
+ .at_least(:once).and_call_original
+
+ subject
+ end
+ end
+
+ context 'with an invalid symbol file' do
+ before do
+ allow(symbol_file).to receive(:read).and_return('invalid')
+ end
+
+ it 'returns an error' do
+ expect(subject).to be_error
+ expect(subject.message).to eq('Could not find the signature in the symbol file')
+ end
+ end
+ end
+end
diff --git a/spec/services/packages/nuget/symbols/extract_symbol_signature_service_spec.rb b/spec/services/packages/nuget/symbols/extract_symbol_signature_service_spec.rb
deleted file mode 100644
index 87b0d00a0a7..00000000000
--- a/spec/services/packages/nuget/symbols/extract_symbol_signature_service_spec.rb
+++ /dev/null
@@ -1,23 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Packages::Nuget::Symbols::ExtractSymbolSignatureService, feature_category: :package_registry do
- let_it_be(:symbol_file) { fixture_file('packages/nuget/symbol/package.pdb') }
-
- let(:service) { described_class.new(symbol_file) }
-
- describe '#execute' do
- subject { service.execute }
-
- context 'with a valid symbol file' do
- it { expect(subject.payload).to eq('b91a152048fc4b3883bf3cf73fbc03f1FFFFFFFF') }
- end
-
- context 'with corrupted data' do
- let(:symbol_file) { 'corrupted data' }
-
- it { expect(subject).to be_error }
- end
- end
-end
diff --git a/spec/services/packages/nuget/update_package_from_metadata_service_spec.rb b/spec/services/packages/nuget/update_package_from_metadata_service_spec.rb
index cb70176ee61..0e19f2ac3f9 100644
--- a/spec/services/packages/nuget/update_package_from_metadata_service_spec.rb
+++ b/spec/services/packages/nuget/update_package_from_metadata_service_spec.rb
@@ -7,7 +7,8 @@ RSpec.describe Packages::Nuget::UpdatePackageFromMetadataService, :clean_gitlab_
let!(:package) { create(:nuget_package, :processing, :with_symbol_package, :with_build) }
let(:package_file) { package.package_files.first }
- let(:service) { described_class.new(package_file) }
+ let(:package_zip_file) { Zip::File.new(package_file.file) }
+ let(:service) { described_class.new(package_file, package_zip_file) }
let(:package_name) { 'DummyProject.DummyPackage' }
let(:package_version) { '1.0.0' }
let(:package_file_name) { 'dummyproject.dummypackage.1.0.0.nupkg' }
@@ -127,7 +128,7 @@ RSpec.describe Packages::Nuget::UpdatePackageFromMetadataService, :clean_gitlab_
context 'with a nuspec file with metadata' do
let(:nuspec_filepath) { 'packages/nuget/with_metadata.nuspec' }
- let(:expected_tags) { %w(foo bar test tag1 tag2 tag3 tag4 tag5) }
+ let(:expected_tags) { %w[foo bar test tag1 tag2 tag3 tag4 tag5] }
before do
allow_next_instance_of(Packages::Nuget::MetadataExtractionService) do |service|
@@ -182,13 +183,15 @@ RSpec.describe Packages::Nuget::UpdatePackageFromMetadataService, :clean_gitlab_
context 'without authors or description' do
%i[authors description].each do |property|
- let(:metadata) { { package_name: package_name, package_version: package_version, property => nil } }
+ context "for #{property}" do
+ let(:metadata) { { package_name: package_name, package_version: package_version, property => nil } }
- before do
- allow(service).to receive(:metadata).and_return(metadata)
- end
+ before do
+ allow(service).to receive(:metadata).and_return(metadata)
+ end
- it_behaves_like 'raising an', described_class::InvalidMetadataError, with_message: described_class::INVALID_METADATA_ERROR_MESSAGE
+ it_behaves_like 'raising an', described_class::InvalidMetadataError, with_message: described_class::INVALID_METADATA_ERROR_MESSAGE
+ end
end
end
end
@@ -245,11 +248,20 @@ RSpec.describe Packages::Nuget::UpdatePackageFromMetadataService, :clean_gitlab_
context 'with existing package' do
let!(:existing_package) { create(:nuget_package, project: package.project, name: package_name, version: package_version) }
let(:package_id) { existing_package.id }
+ let(:package_zip_file) do
+ Zip::File.open(package_file.file.path) do |zipfile|
+ zipfile.add('package.pdb', expand_fixture_path('packages/nuget/symbol/package.pdb'))
+ zipfile
+ end
+ end
it 'link existing package and updates package file', :aggregate_failures do
expect(service).to receive(:try_obtain_lease).and_call_original
expect(::Packages::Nuget::SyncMetadatumService).not_to receive(:new)
expect(::Packages::UpdateTagsService).not_to receive(:new)
+ expect_next_instance_of(Packages::Nuget::Symbols::CreateSymbolFilesService, existing_package, package_zip_file) do |service|
+ expect(service).to receive(:execute).and_call_original
+ end
expect { subject }
.to change { ::Packages::Package.count }.by(-1)
@@ -257,20 +269,11 @@ RSpec.describe Packages::Nuget::UpdatePackageFromMetadataService, :clean_gitlab_
.and change { Packages::DependencyLink.count }.by(0)
.and change { Packages::Nuget::DependencyLinkMetadatum.count }.by(0)
.and change { ::Packages::Nuget::Metadatum.count }.by(0)
+ .and change { existing_package.nuget_symbols.count }.by(1)
expect(package_file.reload.file_name).to eq(package_file_name)
expect(package_file.package).to eq(existing_package)
end
- context 'with packages_nuget_symbols records' do
- before do
- create_list(:nuget_symbol, 2, package: package)
- end
-
- it 'links the symbol records to the existing package' do
- expect { subject }.to change { existing_package.nuget_symbols.count }.by(2)
- end
- end
-
it_behaves_like 'taking the lease'
it_behaves_like 'not updating the package if the lease is taken'
diff --git a/spec/services/packages/protection/create_rule_service_spec.rb b/spec/services/packages/protection/create_rule_service_spec.rb
index 67835479473..ed8d21f4344 100644
--- a/spec/services/packages/protection/create_rule_service_spec.rb
+++ b/spec/services/packages/protection/create_rule_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Packages::Protection::CreateRuleService, '#execute', feature_category: :environment_management do
+RSpec.describe Packages::Protection::CreateRuleService, '#execute', feature_category: :package_registry do
let_it_be(:project) { create(:project, :repository) }
let_it_be(:maintainer) { create(:user).tap { |u| project.add_maintainer(u) } }
diff --git a/spec/services/packages/protection/delete_rule_service_spec.rb b/spec/services/packages/protection/delete_rule_service_spec.rb
new file mode 100644
index 00000000000..d64609d4df1
--- /dev/null
+++ b/spec/services/packages/protection/delete_rule_service_spec.rb
@@ -0,0 +1,92 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Packages::Protection::DeleteRuleService, '#execute', feature_category: :package_registry do
+ let_it_be(:project) { create(:project) }
+ let_it_be(:current_user) { create(:user, maintainer_projects: [project]) }
+ let_it_be_with_refind(:package_protection_rule) { create(:package_protection_rule, project: project) }
+
+ subject { described_class.new(package_protection_rule, current_user: current_user).execute }
+
+ shared_examples 'a successful service response' do
+ it { is_expected.to be_success }
+
+ it {
+ is_expected.to have_attributes(
+ errors: be_blank,
+ message: be_blank,
+ payload: { package_protection_rule: package_protection_rule }
+ )
+ }
+
+ it { subject.tap { expect { package_protection_rule.reload }.to raise_error ActiveRecord::RecordNotFound } }
+ end
+
+ shared_examples 'an erroneous service response' do
+ it { is_expected.to be_error }
+ it { is_expected.to have_attributes(message: be_present, payload: { package_protection_rule: be_blank }) }
+
+ it do
+ expect { subject }.not_to change { Packages::Protection::Rule.count }
+
+ expect { package_protection_rule.reload }.not_to raise_error
+ end
+ end
+
+ it_behaves_like 'a successful service response'
+
+ it 'deletes the package protection rule in the database' do
+ expect { subject }
+ .to change { project.reload.package_protection_rules }.from([package_protection_rule]).to([])
+ .and change { ::Packages::Protection::Rule.count }.from(1).to(0)
+ end
+
+ context 'with deleted package protection rule' do
+ let!(:package_protection_rule) do
+ create(:package_protection_rule, project: project, package_name_pattern: 'protection_rule_deleted').destroy!
+ end
+
+ it_behaves_like 'a successful service response'
+ end
+
+ context 'when error occurs during delete operation' do
+ before do
+ allow(package_protection_rule).to receive(:destroy!).and_raise(StandardError.new('Some error'))
+ end
+
+ it_behaves_like 'an erroneous service response'
+
+ it { is_expected.to have_attributes message: /Some error/ }
+ end
+
+ context 'when current_user does not have permission' do
+ let_it_be(:developer) { create(:user).tap { |u| project.add_developer(u) } }
+ let_it_be(:reporter) { create(:user).tap { |u| project.add_reporter(u) } }
+ let_it_be(:guest) { create(:user).tap { |u| project.add_guest(u) } }
+ let_it_be(:anonymous) { create(:user) }
+
+ where(:current_user) do
+ [ref(:developer), ref(:reporter), ref(:guest), ref(:anonymous)]
+ end
+
+ with_them do
+ it_behaves_like 'an erroneous service response'
+
+ it { is_expected.to have_attributes message: /Unauthorized to delete a package protection rule/ }
+ end
+ end
+
+ context 'without package protection rule' do
+ let(:package_protection_rule) { nil }
+
+ it { expect { subject }.to raise_error(ArgumentError) }
+ end
+
+ context 'without current_user' do
+ let(:current_user) { nil }
+ let(:package_protection_rule) { build_stubbed(:package_protection_rule, project: project) }
+
+ it { expect { subject }.to raise_error(ArgumentError) }
+ end
+end
diff --git a/spec/services/packages/pypi/create_package_service_spec.rb b/spec/services/packages/pypi/create_package_service_spec.rb
index 0d278e32e89..abff91d1878 100644
--- a/spec/services/packages/pypi/create_package_service_spec.rb
+++ b/spec/services/packages/pypi/create_package_service_spec.rb
@@ -69,6 +69,30 @@ RSpec.describe Packages::Pypi::CreatePackageService, :aggregate_failures, featur
end
end
+ context 'with additional metadata' do
+ before do
+ params.merge!(
+ metadata_version: '2.3',
+ author_email: 'cschultz@example.com, snoopy@peanuts.com',
+ description: 'Example description',
+ description_content_type: 'text/plain',
+ summary: 'A module for collecting votes from beagles.',
+ keywords: 'dog,puppy,voting,election'
+ )
+ end
+
+ it 'creates the package' do
+ expect { subject }.to change { Packages::Package.pypi.count }.by(1)
+
+ expect(created_package.pypi_metadatum.metadata_version).to eq('2.3')
+ expect(created_package.pypi_metadatum.author_email).to eq('cschultz@example.com, snoopy@peanuts.com')
+ expect(created_package.pypi_metadatum.description).to eq('Example description')
+ expect(created_package.pypi_metadatum.description_content_type).to eq('text/plain')
+ expect(created_package.pypi_metadatum.summary).to eq('A module for collecting votes from beagles.')
+ expect(created_package.pypi_metadatum.keywords).to eq('dog,puppy,voting,election')
+ end
+ end
+
context 'with an invalid metadata' do
let(:requires_python) { 'x' * 256 }
diff --git a/spec/services/packages/update_tags_service_spec.rb b/spec/services/packages/update_tags_service_spec.rb
index d8f572fff32..ec4d68ba5a0 100644
--- a/spec/services/packages/update_tags_service_spec.rb
+++ b/spec/services/packages/update_tags_service_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
RSpec.describe Packages::UpdateTagsService, feature_category: :package_registry do
let_it_be(:package, reload: true) { create(:nuget_package) }
- let(:tags) { %w(test-tag tag1 tag2 tag3) }
+ let(:tags) { %w[test-tag tag1 tag2 tag3] }
let(:service) { described_class.new(package, tags) }
describe '#execute' do
diff --git a/spec/services/pages/delete_service_spec.rb b/spec/services/pages/delete_service_spec.rb
index 488f29f6b7e..86b1bd5bde2 100644
--- a/spec/services/pages/delete_service_spec.rb
+++ b/spec/services/pages/delete_service_spec.rb
@@ -8,14 +8,12 @@ RSpec.describe Pages::DeleteService, feature_category: :pages do
let(:project) { create(:project, path: "my.project") }
let(:service) { described_class.new(project, admin) }
- before do
- project.mark_pages_as_deployed
- end
-
it 'marks pages as not deployed' do
- expect do
- service.execute
- end.to change { project.reload.pages_deployed? }.from(true).to(false)
+ create(:pages_deployment, project: project)
+
+ expect { service.execute }
+ .to change { project.reload.pages_deployed? }
+ .from(true).to(false)
end
it 'deletes all domains' do
@@ -29,9 +27,8 @@ RSpec.describe Pages::DeleteService, feature_category: :pages do
end
it 'schedules a destruction of pages deployments' do
- expect(DestroyPagesDeploymentsWorker).to(
- receive(:perform_async).with(project.id)
- )
+ expect(DestroyPagesDeploymentsWorker)
+ .to(receive(:perform_async).with(project.id))
service.execute
end
@@ -39,9 +36,8 @@ RSpec.describe Pages::DeleteService, feature_category: :pages do
it 'removes pages deployments', :sidekiq_inline do
create(:pages_deployment, project: project)
- expect do
- service.execute
- end.to change { PagesDeployment.count }.by(-1)
+ expect { service.execute }
+ .to change { PagesDeployment.count }.by(-1)
end
it 'publishes a ProjectDeleted event with project id and namespace id' do
diff --git a/spec/services/pages_domains/obtain_lets_encrypt_certificate_service_spec.rb b/spec/services/pages_domains/obtain_lets_encrypt_certificate_service_spec.rb
index 7f8992e8bbc..0e46391c0ad 100644
--- a/spec/services/pages_domains/obtain_lets_encrypt_certificate_service_spec.rb
+++ b/spec/services/pages_domains/obtain_lets_encrypt_certificate_service_spec.rb
@@ -65,7 +65,7 @@ RSpec.describe PagesDomains::ObtainLetsEncryptCertificateService, feature_catego
end
end
- %w(pending processing).each do |status|
+ %w[pending processing].each do |status|
context "there is an order in '#{status}' status" do
let(:existing_order) do
create(:pages_domain_acme_order, pages_domain: pages_domain)
diff --git a/spec/services/product_analytics/build_graph_service_spec.rb b/spec/services/product_analytics/build_graph_service_spec.rb
index 13c7206241c..a850d69e53c 100644
--- a/spec/services/product_analytics/build_graph_service_spec.rb
+++ b/spec/services/product_analytics/build_graph_service_spec.rb
@@ -21,7 +21,7 @@ RSpec.describe ProductAnalytics::BuildGraphService, feature_category: :product_a
it 'returns a valid graph hash' do
expect(subject[:id]).to eq(:platform)
- expect(subject[:keys]).to eq(%w(app mobile web))
+ expect(subject[:keys]).to eq(%w[app mobile web])
expect(subject[:values]).to eq([1, 1, 2])
end
end
diff --git a/spec/services/projects/branches_by_mode_service_spec.rb b/spec/services/projects/branches_by_mode_service_spec.rb
index bfe76b34310..c87787346b9 100644
--- a/spec/services/projects/branches_by_mode_service_spec.rb
+++ b/spec/services/projects/branches_by_mode_service_spec.rb
@@ -50,7 +50,7 @@ RSpec.describe Projects::BranchesByModeService, feature_category: :source_code_m
branches, prev_page, next_page = subject
- expect(branches.map(&:name)).to match_array(%w(feature feature_conflict))
+ expect(branches.map(&:name)).to match_array(%w[feature feature_conflict])
expect(next_page).to be_nil
expect(prev_page).to be_nil
end
diff --git a/spec/services/projects/container_repository/delete_tags_service_spec.rb b/spec/services/projects/container_repository/delete_tags_service_spec.rb
index 5b67d614dfb..942e244e3d2 100644
--- a/spec/services/projects/container_repository/delete_tags_service_spec.rb
+++ b/spec/services/projects/container_repository/delete_tags_service_spec.rb
@@ -77,7 +77,7 @@ RSpec.describe Projects::ContainerRepository::DeleteTagsService, feature_categor
before do
expect(::Projects::ContainerRepository::Gitlab::DeleteTagsService).not_to receive(:new)
expect(::Projects::ContainerRepository::ThirdParty::DeleteTagsService).not_to receive(:new)
- expect_any_instance_of(ContainerRegistry::Client).not_to receive(:delete_repository_tag_by_name)
+ expect_any_instance_of(ContainerRegistry::Client).not_to receive(:delete_repository_tag_by_digest)
end
context 'when no params are specified' do
@@ -107,7 +107,7 @@ RSpec.describe Projects::ContainerRepository::DeleteTagsService, feature_categor
context 'with the real service' do
before do
stub_delete_reference_requests(tags)
- expect_delete_tag_by_names(tags)
+ expect_delete_tags(tags)
end
it { is_expected.to include(status: :success) }
diff --git a/spec/services/projects/container_repository/gitlab/delete_tags_service_spec.rb b/spec/services/projects/container_repository/gitlab/delete_tags_service_spec.rb
index 0d7d1254428..676c7ca8e99 100644
--- a/spec/services/projects/container_repository/gitlab/delete_tags_service_spec.rb
+++ b/spec/services/projects/container_repository/gitlab/delete_tags_service_spec.rb
@@ -15,7 +15,7 @@ RSpec.describe Projects::ContainerRepository::Gitlab::DeleteTagsService, feature
RSpec.shared_examples 'deleting tags' do
it 'deletes the tags by name' do
stub_delete_reference_requests(tags)
- expect_delete_tag_by_names(tags)
+ expect_delete_tags(tags)
is_expected.to eq(status: :success, deleted: tags)
end
@@ -59,7 +59,7 @@ RSpec.describe Projects::ContainerRepository::Gitlab::DeleteTagsService, feature
tags.each do |tag|
stub_delete_reference_requests(tag => 500)
end
- allow(container_repository).to receive(:delete_tag_by_name).and_return(false)
+ allow(container_repository).to receive(:delete_tag).and_return(false)
end
it 'truncates the log message' do
@@ -119,7 +119,7 @@ RSpec.describe Projects::ContainerRepository::Gitlab::DeleteTagsService, feature
let_it_be(:tags) { [] }
it 'does not remove anything' do
- expect_any_instance_of(ContainerRegistry::Client).not_to receive(:delete_repository_tag_by_name)
+ expect_any_instance_of(ContainerRegistry::Client).not_to receive(:delete_repository_tag_by_digest)
is_expected.to eq(status: :success, deleted: [])
end
diff --git a/spec/services/projects/container_repository/third_party/delete_tags_service_spec.rb b/spec/services/projects/container_repository/third_party/delete_tags_service_spec.rb
index 0c297b6e1f7..d3d3f3bb7ce 100644
--- a/spec/services/projects/container_repository/third_party/delete_tags_service_spec.rb
+++ b/spec/services/projects/container_repository/third_party/delete_tags_service_spec.rb
@@ -18,7 +18,7 @@ RSpec.describe Projects::ContainerRepository::ThirdParty::DeleteTagsService, fea
tags.each { |tag| stub_put_manifest_request(tag) }
- expect_delete_tag_by_digest('sha256:dummy')
+ expect_delete_tags(['sha256:dummy'])
is_expected.to eq(status: :success, deleted: tags)
end
@@ -92,7 +92,7 @@ RSpec.describe Projects::ContainerRepository::ThirdParty::DeleteTagsService, fea
let_it_be(:tags) { [] }
it 'does not remove anything' do
- expect_any_instance_of(ContainerRegistry::Client).not_to receive(:delete_repository_tag_by_name)
+ expect_any_instance_of(ContainerRegistry::Client).not_to receive(:delete_repository_tag_by_digest)
is_expected.to eq(status: :success, deleted: [])
end
diff --git a/spec/services/projects/create_service_spec.rb b/spec/services/projects/create_service_spec.rb
index ce7e5188c7b..899ed477180 100644
--- a/spec/services/projects/create_service_spec.rb
+++ b/spec/services/projects/create_service_spec.rb
@@ -320,7 +320,7 @@ RSpec.describe Projects::CreateService, '#execute', feature_category: :groups_an
it 'cannot create a project' do
expect(project.errors.errors.length).to eq 1
- expect(project.errors.messages[:limit_reached].first).to eq(_('Personal project creation is not allowed. Please contact your administrator with questions'))
+ expect(project.errors.messages[:limit_reached].first).to eq(_('You cannot create projects in your personal namespace. Contact your GitLab administrator.'))
end
end
diff --git a/spec/services/projects/destroy_service_spec.rb b/spec/services/projects/destroy_service_spec.rb
index 0210e101e5f..a0064eadf13 100644
--- a/spec/services/projects/destroy_service_spec.rb
+++ b/spec/services/projects/destroy_service_spec.rb
@@ -472,6 +472,31 @@ RSpec.describe Projects::DestroyService, :aggregate_failures, :event_store_publi
end
end
+ context 'with related storage move records' do
+ context 'when project has active repository storage move records' do
+ let!(:project_repository_storage_move) { create(:project_repository_storage_move, :scheduled, container: project) }
+
+ it 'does not delete the project' do
+ expect(destroy_project(project, user)).to be_falsey
+
+ expect(project.delete_error).to eq "Couldn't remove the project. A project repository storage move is in progress. Try again when it's complete."
+ expect(project.pending_delete).to be_falsey
+ end
+ end
+
+ context 'when project has active snippet storage move records' do
+ let(:project_snippet) { create(:project_snippet, project: project) }
+ let!(:snippet_repository_storage_move) { create(:snippet_repository_storage_move, :started, container: project_snippet) }
+
+ it 'does not delete the project' do
+ expect(destroy_project(project, user)).to be_falsey
+
+ expect(project.delete_error).to eq "Couldn't remove the project. A related snippet repository storage move is in progress. Try again when it's complete."
+ expect(project.pending_delete).to be_falsey
+ end
+ end
+ end
+
context 'repository removal' do
describe '.trash_project_repositories!' do
let(:trash_project_repositories!) { described_class.new(project, user, {}).send(:trash_project_repositories!) }
diff --git a/spec/services/projects/fork_service_spec.rb b/spec/services/projects/fork_service_spec.rb
index 4d55f310974..ceb060445ad 100644
--- a/spec/services/projects/fork_service_spec.rb
+++ b/spec/services/projects/fork_service_spec.rb
@@ -510,6 +510,26 @@ RSpec.describe Projects::ForkService, feature_category: :source_code_management
end
end
+ describe '#valid_fork_branch?' do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:project) { create(:project, :small_repo, creator_id: user.id) }
+ let_it_be(:branch) { nil }
+
+ subject { described_class.new(project, user).valid_fork_branch?(branch) }
+
+ context 'when branch exists' do
+ let(:branch) { project.default_branch_or_main }
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'when branch does not exist' do
+ let(:branch) { 'branch-that-does-not-exist' }
+
+ it { is_expected.to be_falsey }
+ end
+ end
+
describe '#valid_fork_target?' do
let(:project) { Project.new }
let(:params) { {} }
diff --git a/spec/services/projects/group_links/create_service_spec.rb b/spec/services/projects/group_links/create_service_spec.rb
index ca2902af472..e3f170ef3fe 100644
--- a/spec/services/projects/group_links/create_service_spec.rb
+++ b/spec/services/projects/group_links/create_service_spec.rb
@@ -6,6 +6,7 @@ RSpec.describe Projects::GroupLinks::CreateService, '#execute', feature_category
let_it_be(:user) { create :user }
let_it_be(:group) { create :group }
let_it_be(:project) { create(:project, namespace: create(:namespace, :with_namespace_settings)) }
+ let_it_be(:group_user) { create(:user).tap { |user| group.add_guest(user) } }
let(:opts) do
{
@@ -37,67 +38,75 @@ RSpec.describe Projects::GroupLinks::CreateService, '#execute', feature_category
end
end
- context 'when user has proper membership to share a group' do
+ context 'when user has proper permissions to share a project with a group' do
before do
group.add_guest(user)
end
- it_behaves_like 'shareable'
-
- it 'updates authorization', :sidekiq_inline do
- expect { subject.execute }.to(
- change { Ability.allowed?(user, :read_project, project) }
- .from(false).to(true))
- end
-
- context 'with specialized project_authorization workers' do
- let_it_be(:other_user) { create(:user) }
-
+ context 'when the user is a MAINTAINER in the project' do
before do
- group.add_developer(other_user)
+ project.add_maintainer(user)
end
- it 'schedules authorization update for users with access to group' do
- stub_feature_flags(do_not_run_safety_net_auth_refresh_jobs: false)
-
- expect(AuthorizedProjectsWorker).not_to(
- receive(:bulk_perform_async)
- )
- expect(AuthorizedProjectUpdate::ProjectRecalculateWorker).to(
- receive(:perform_async)
- .with(project.id)
- .and_call_original
- )
- expect(AuthorizedProjectUpdate::UserRefreshFromReplicaWorker).to(
- receive(:bulk_perform_in).with(
- 1.hour,
- array_including([user.id], [other_user.id]),
- batch_delay: 30.seconds, batch_size: 100
- ).and_call_original
- )
-
- subject.execute
+ it_behaves_like 'shareable'
+
+ it 'updates authorization', :sidekiq_inline do
+ expect { subject.execute }.to(
+ change { Ability.allowed?(group_user, :read_project, project) }
+ .from(false).to(true))
end
- end
- context 'when sharing outside the hierarchy is disabled' do
- let_it_be(:shared_group_parent) do
- create(:group, namespace_settings: create(:namespace_settings, prevent_sharing_groups_outside_hierarchy: true))
+ context 'with specialized project_authorization workers' do
+ let_it_be(:other_user) { create(:user) }
+
+ before do
+ group.add_developer(other_user)
+ end
+
+ it 'schedules authorization update for users with access to group' do
+ stub_feature_flags(do_not_run_safety_net_auth_refresh_jobs: false)
+
+ expect(AuthorizedProjectsWorker).not_to(
+ receive(:bulk_perform_async)
+ )
+ expect(AuthorizedProjectUpdate::ProjectRecalculateWorker).to(
+ receive(:perform_async)
+ .with(project.id)
+ .and_call_original
+ )
+ expect(AuthorizedProjectUpdate::UserRefreshFromReplicaWorker).to(
+ receive(:bulk_perform_in).with(
+ 1.hour,
+ array_including([user.id], [other_user.id]),
+ batch_delay: 30.seconds, batch_size: 100
+ ).and_call_original
+ )
+
+ subject.execute
+ end
end
- let_it_be(:project, reload: true) { create(:project, group: shared_group_parent) }
+ context 'when sharing outside the hierarchy is disabled' do
+ let_it_be(:shared_group_parent) do
+ create(:group,
+ namespace_settings: create(:namespace_settings, prevent_sharing_groups_outside_hierarchy: true)
+ )
+ end
+
+ let_it_be(:project, reload: true) { create(:project, group: shared_group_parent) }
- it_behaves_like 'not shareable'
+ it_behaves_like 'not shareable'
- context 'when group is inside hierarchy' do
- let(:group) { create(:group, :private, parent: shared_group_parent) }
+ context 'when group is inside hierarchy' do
+ let(:group) { create(:group, :private, parent: shared_group_parent) }
- it_behaves_like 'shareable'
+ it_behaves_like 'shareable'
+ end
end
end
end
- context 'when user does not have permissions for the group' do
+ context 'when user does not have permissions to share the project with a group' do
it_behaves_like 'not shareable'
end
diff --git a/spec/services/projects/group_links/destroy_service_spec.rb b/spec/services/projects/group_links/destroy_service_spec.rb
index 103aff8c659..0cd003f6142 100644
--- a/spec/services/projects/group_links/destroy_service_spec.rb
+++ b/spec/services/projects/group_links/destroy_service_spec.rb
@@ -6,83 +6,120 @@ RSpec.describe Projects::GroupLinks::DestroyService, '#execute', feature_categor
let_it_be(:user) { create :user }
let_it_be(:project) { create(:project, :private) }
let_it_be(:group) { create(:group) }
+ let_it_be(:group_user) { create(:user).tap { |user| group.add_guest(user) } }
- let!(:group_link) { create(:project_group_link, project: project, group: group) }
+ let(:group_access) { Gitlab::Access::DEVELOPER }
+ let!(:group_link) { create(:project_group_link, project: project, group: group, group_access: group_access) }
subject { described_class.new(project, user) }
- it 'removes group from project' do
- expect { subject.execute(group_link) }.to change { project.project_group_links.count }.from(1).to(0)
- end
-
- context 'project authorizations refresh' do
- before do
- group.add_maintainer(user)
+ shared_examples_for 'removes group from project' do
+ it 'removes group from project' do
+ expect { subject.execute(group_link) }.to change { project.reload.project_group_links.count }.from(1).to(0)
end
+ end
- it 'calls AuthorizedProjectUpdate::ProjectRecalculateWorker to update project authorizations' do
- expect(AuthorizedProjectUpdate::ProjectRecalculateWorker)
- .to receive(:perform_async).with(group_link.project.id)
+ shared_examples_for 'returns not_found' do
+ it do
+ expect do
+ result = subject.execute(group_link)
- subject.execute(group_link)
+ expect(result[:status]).to eq(:error)
+ expect(result[:reason]).to eq(:not_found)
+ end.not_to change { project.reload.project_group_links.count }
end
+ end
- it 'calls AuthorizedProjectUpdate::UserRefreshFromReplicaWorker with a delay to update project authorizations' do
- stub_feature_flags(do_not_run_safety_net_auth_refresh_jobs: false)
-
- expect(AuthorizedProjectUpdate::UserRefreshFromReplicaWorker).to(
- receive(:bulk_perform_in).with(
- 1.hour,
- [[user.id]],
- batch_delay: 30.seconds, batch_size: 100
- )
- )
-
- subject.execute(group_link)
- end
+ context 'if group_link is blank' do
+ let!(:group_link) { nil }
- it 'updates project authorizations of users who had access to the project via the group share', :sidekiq_inline do
- expect { subject.execute(group_link) }.to(
- change { Ability.allowed?(user, :read_project, project) }
- .from(true).to(false))
- end
+ it_behaves_like 'returns not_found'
end
- it 'returns false if group_link is blank' do
- expect { subject.execute(nil) }.not_to change { project.project_group_links.count }
+ context 'if the user does not have access to destroy the link' do
+ it_behaves_like 'returns not_found'
end
- describe 'todos cleanup' do
- context 'when project is private' do
- it 'triggers todos cleanup' do
- expect(TodosDestroyer::ProjectPrivateWorker).to receive(:perform_in).with(Todo::WAIT_FOR_DELETE, project.id)
- expect(project.private?).to be true
-
- subject.execute(group_link)
+ context 'when the user has proper permissions to remove a group-link from a project' do
+ context 'when the user is a MAINTAINER in the project' do
+ before do
+ project.add_maintainer(user)
end
- end
- context 'when project is public or internal' do
- shared_examples_for 'removes confidential todos' do
- it 'does not trigger todos cleanup' do
- expect(TodosDestroyer::ProjectPrivateWorker).not_to receive(:perform_in).with(Todo::WAIT_FOR_DELETE, project.id)
- expect(TodosDestroyer::ConfidentialIssueWorker).to receive(:perform_in).with(Todo::WAIT_FOR_DELETE, nil, project.id)
- expect(project.private?).to be false
+ it_behaves_like 'removes group from project'
+
+ context 'project authorizations refresh' do
+ it 'calls AuthorizedProjectUpdate::ProjectRecalculateWorker to update project authorizations' do
+ expect(AuthorizedProjectUpdate::ProjectRecalculateWorker)
+ .to receive(:perform_async).with(group_link.project.id)
subject.execute(group_link)
end
- end
- context 'when project is public' do
- let(:project) { create(:project, :public) }
+ it 'calls AuthorizedProjectUpdate::UserRefreshFromReplicaWorker with a delay to update project authorizations' do
+ stub_feature_flags(do_not_run_safety_net_auth_refresh_jobs: false)
- it_behaves_like 'removes confidential todos'
+ expect(AuthorizedProjectUpdate::UserRefreshFromReplicaWorker).to(
+ receive(:bulk_perform_in).with(
+ 1.hour,
+ [[group_user.id]],
+ batch_delay: 30.seconds, batch_size: 100
+ )
+ )
+
+ subject.execute(group_link)
+ end
+
+ it 'updates project authorizations of users who had access to the project via the group share', :sidekiq_inline do
+ expect { subject.execute(group_link) }.to(
+ change { Ability.allowed?(group_user, :read_project, project) }
+ .from(true).to(false))
+ end
end
- context 'when project is internal' do
- let(:project) { create(:project, :public) }
+ describe 'todos cleanup' do
+ context 'when project is private' do
+ it 'triggers todos cleanup' do
+ expect(TodosDestroyer::ProjectPrivateWorker).to receive(:perform_in).with(Todo::WAIT_FOR_DELETE, project.id)
+ expect(project.private?).to be true
+
+ subject.execute(group_link)
+ end
+ end
+
+ context 'when project is public or internal' do
+ shared_examples_for 'removes confidential todos' do
+ it 'does not trigger todos cleanup' do
+ expect(TodosDestroyer::ProjectPrivateWorker).not_to receive(:perform_in).with(Todo::WAIT_FOR_DELETE, project.id)
+ expect(TodosDestroyer::ConfidentialIssueWorker).to receive(:perform_in).with(Todo::WAIT_FOR_DELETE, nil, project.id)
+ expect(project.private?).to be false
+
+ subject.execute(group_link)
+ end
+ end
+
+ context 'when project is public' do
+ let(:project) { create(:project, :public) }
+
+ it_behaves_like 'removes confidential todos'
+ end
+
+ context 'when project is internal' do
+ let(:project) { create(:project, :public) }
+
+ it_behaves_like 'removes confidential todos'
+ end
+ end
+ end
+ end
+ end
- it_behaves_like 'removes confidential todos'
+ context 'when skipping authorization' do
+ context 'without providing a user' do
+ it 'destroys the link' do
+ expect do
+ described_class.new(project, nil).execute(group_link, skip_authorization: true)
+ end.to change { project.reload.project_group_links.count }.by(-1)
end
end
end
diff --git a/spec/services/projects/group_links/update_service_spec.rb b/spec/services/projects/group_links/update_service_spec.rb
index f7607deef04..b02614fa062 100644
--- a/spec/services/projects/group_links/update_service_spec.rb
+++ b/spec/services/projects/group_links/update_service_spec.rb
@@ -6,8 +6,11 @@ RSpec.describe Projects::GroupLinks::UpdateService, '#execute', feature_category
let_it_be(:user) { create :user }
let_it_be(:group) { create :group }
let_it_be(:project) { create :project }
+ let_it_be(:group_user) { create(:user).tap { |user| group.add_developer(user) } }
- let!(:link) { create(:project_group_link, project: project, group: group) }
+ let(:group_access) { Gitlab::Access::DEVELOPER }
+
+ let!(:link) { create(:project_group_link, project: project, group: group, group_access: group_access) }
let(:expiry_date) { 1.month.from_now.to_date }
let(:group_link_params) do
@@ -17,60 +20,78 @@ RSpec.describe Projects::GroupLinks::UpdateService, '#execute', feature_category
subject { described_class.new(link, user).execute(group_link_params) }
- before do
- group.add_developer(user)
- end
-
- it 'updates existing link' do
- expect(link.group_access).to eq(Gitlab::Access::DEVELOPER)
- expect(link.expires_at).to be_nil
-
- subject
-
- link.reload
+ shared_examples_for 'returns not_found' do
+ it do
+ result = subject
- expect(link.group_access).to eq(Gitlab::Access::GUEST)
- expect(link.expires_at).to eq(expiry_date)
- end
-
- context 'project authorizations update' do
- it 'calls AuthorizedProjectUpdate::ProjectRecalculateWorker to update project authorizations' do
- expect(AuthorizedProjectUpdate::ProjectRecalculateWorker)
- .to receive(:perform_async).with(link.project.id)
-
- subject
- end
-
- it 'calls AuthorizedProjectUpdate::UserRefreshFromReplicaWorker with a delay to update project authorizations' do
- stub_feature_flags(do_not_run_safety_net_auth_refresh_jobs: false)
-
- expect(AuthorizedProjectUpdate::UserRefreshFromReplicaWorker).to(
- receive(:bulk_perform_in).with(
- 1.hour,
- [[user.id]],
- batch_delay: 30.seconds, batch_size: 100
- )
- )
-
- subject
- end
-
- it 'updates project authorizations of users who had access to the project via the group share', :sidekiq_inline do
- group.add_maintainer(user)
-
- expect { subject }.to(
- change { Ability.allowed?(user, :create_release, project) }
- .from(true).to(false))
+ expect(result[:status]).to eq(:error)
+ expect(result[:reason]).to eq(:not_found)
end
end
- context 'with only param not requiring authorization refresh' do
- let(:group_link_params) { { expires_at: Date.tomorrow } }
-
- it 'does not perform any project authorizations update using `AuthorizedProjectUpdate::ProjectRecalculateWorker`' do
- expect(AuthorizedProjectUpdate::ProjectRecalculateWorker).not_to receive(:perform_async)
+ context 'when the user does not have proper permissions to update a project group link' do
+ it_behaves_like 'returns not_found'
+ end
- subject
+ context 'when user has proper permissions to update a project group link' do
+ context 'when the user is a MAINTAINER in the project' do
+ before do
+ project.add_maintainer(user)
+ end
+
+ it 'updates existing link' do
+ expect(link.group_access).to eq(Gitlab::Access::DEVELOPER)
+ expect(link.expires_at).to be_nil
+
+ subject
+
+ link.reload
+
+ expect(link.group_access).to eq(Gitlab::Access::GUEST)
+ expect(link.expires_at).to eq(expiry_date)
+ end
+
+ context 'project authorizations update' do
+ it 'calls AuthorizedProjectUpdate::ProjectRecalculateWorker to update project authorizations' do
+ expect(AuthorizedProjectUpdate::ProjectRecalculateWorker)
+ .to receive(:perform_async).with(link.project.id)
+
+ subject
+ end
+
+ it 'calls AuthorizedProjectUpdate::UserRefreshFromReplicaWorker ' \
+ 'with a delay to update project authorizations' do
+ stub_feature_flags(do_not_run_safety_net_auth_refresh_jobs: false)
+
+ expect(AuthorizedProjectUpdate::UserRefreshFromReplicaWorker).to(
+ receive(:bulk_perform_in).with(
+ 1.hour,
+ [[group_user.id]],
+ batch_delay: 30.seconds, batch_size: 100
+ )
+ )
+
+ subject
+ end
+
+ it 'updates project authorizations of users who had access to the project via the group share',
+ :sidekiq_inline do
+ expect { subject }.to(
+ change { Ability.allowed?(group_user, :developer_access, project) }
+ .from(true).to(false))
+ end
+ end
+
+ context 'with only param not requiring authorization refresh' do
+ let(:group_link_params) { { expires_at: Date.tomorrow } }
+
+ it 'does not perform any project authorizations update using ' \
+ '`AuthorizedProjectUpdate::ProjectRecalculateWorker`' do
+ expect(AuthorizedProjectUpdate::ProjectRecalculateWorker).not_to receive(:perform_async)
+
+ subject
+ end
+ end
end
end
end
diff --git a/spec/services/projects/lfs_pointers/lfs_link_service_spec.rb b/spec/services/projects/lfs_pointers/lfs_link_service_spec.rb
index fb3cc9bdac9..d3f053aaedc 100644
--- a/spec/services/projects/lfs_pointers/lfs_link_service_spec.rb
+++ b/spec/services/projects/lfs_pointers/lfs_link_service_spec.rb
@@ -73,10 +73,10 @@ RSpec.describe Projects::LfsPointers::LfsLinkService, feature_category: :source_
it 'only queries for the batch that will be processed', :aggregate_failures do
stub_const("#{described_class}::BATCH_SIZE", 1)
- oids = %w(one two)
+ oids = %w[one two]
- expect(LfsObject).to receive(:for_oids).with(%w(one)).once.and_call_original
- expect(LfsObject).to receive(:for_oids).with(%w(two)).once.and_call_original
+ expect(LfsObject).to receive(:for_oids).with(%w[one]).once.and_call_original
+ expect(LfsObject).to receive(:for_oids).with(%w[two]).once.and_call_original
subject.execute(oids)
end
diff --git a/spec/services/projects/operations/update_service_spec.rb b/spec/services/projects/operations/update_service_spec.rb
index 5f9b1a59bf9..03508a9732e 100644
--- a/spec/services/projects/operations/update_service_spec.rb
+++ b/spec/services/projects/operations/update_service_spec.rb
@@ -325,7 +325,7 @@ RSpec.describe Projects::Operations::UpdateService, feature_category: :groups_an
expect(project_arg).to eq project
expect(user_arg).to eq user
expect(prometheus_attrs).to have_key('encrypted_properties')
- expect(prometheus_attrs.keys).not_to include(*%w(id project_id created_at updated_at properties))
+ expect(prometheus_attrs.keys).not_to include(*%w[id project_id created_at updated_at properties])
expect(prometheus_attrs['encrypted_properties']).not_to eq(prometheus_integration.encrypted_properties)
end.and_call_original
diff --git a/spec/services/projects/record_target_platforms_service_spec.rb b/spec/services/projects/record_target_platforms_service_spec.rb
index bf87b763341..40ade386847 100644
--- a/spec/services/projects/record_target_platforms_service_spec.rb
+++ b/spec/services/projects/record_target_platforms_service_spec.rb
@@ -21,11 +21,11 @@ RSpec.describe Projects::RecordTargetPlatformsService, '#execute', feature_categ
it 'creates a new setting record for the project', :aggregate_failures do
expect { execute }.to change { ProjectSetting.count }.from(0).to(1)
- expect(ProjectSetting.last.target_platforms).to match_array(%w(ios osx))
+ expect(ProjectSetting.last.target_platforms).to match_array(%w[ios osx])
end
it 'returns array of detected target platforms' do
- expect(execute).to match_array %w(ios osx)
+ expect(execute).to match_array %w[ios osx]
end
context 'when a project has an existing setting record' do
@@ -34,17 +34,17 @@ RSpec.describe Projects::RecordTargetPlatformsService, '#execute', feature_categ
end
context 'when target platforms changed' do
- let(:saved_target_platforms) { %w(tvos) }
+ let(:saved_target_platforms) { %w[tvos] }
it 'updates' do
- expect { execute }.to change { project_setting.target_platforms }.from(%w(tvos)).to(%w(ios osx))
+ expect { execute }.to change { project_setting.target_platforms }.from(%w[tvos]).to(%w[ios osx])
end
- it { is_expected.to match_array %w(ios osx) }
+ it { is_expected.to match_array %w[ios osx] }
end
context 'when target platforms are the same' do
- let(:saved_target_platforms) { %w(osx ios) }
+ let(:saved_target_platforms) { %w[osx ios] }
it 'does not update' do
expect { execute }.not_to change { project_setting.updated_at }
diff --git a/spec/services/projects/update_pages_service_spec.rb b/spec/services/projects/update_pages_service_spec.rb
index 0ad7693a047..b5d1276988f 100644
--- a/spec/services/projects/update_pages_service_spec.rb
+++ b/spec/services/projects/update_pages_service_spec.rb
@@ -48,7 +48,7 @@ RSpec.describe Projects::UpdatePagesService, feature_category: :pages do
deploy_status = GenericCommitStatus.last
expect(deploy_status.description).not_to be_present
- expect(project.pages_metadatum).to be_deployed
+ expect(project.pages_deployed?).to eq(true)
end
it_behaves_like 'old deployments'
@@ -116,15 +116,14 @@ RSpec.describe Projects::UpdatePagesService, feature_category: :pages do
it "doesn't delete artifacts after deploying" do
expect(service.execute[:status]).to eq(:success)
- expect(project.pages_metadatum).to be_deployed
+ expect(project.pages_deployed?).to eq(true)
expect(build.artifacts?).to eq(true)
end
it 'succeeds' do
- expect(project.pages_deployed?).to be_falsey
- expect(service.execute[:status]).to eq(:success)
- expect(project.pages_metadatum).to be_deployed
- expect(project.pages_deployed?).to be_truthy
+ expect { expect(service.execute[:status]).to eq(:success) }
+ .to change { project.pages_deployed? }
+ .from(false).to(true)
end
it 'publishes a PageDeployedEvent event with project id and namespace id' do
@@ -137,10 +136,10 @@ RSpec.describe Projects::UpdatePagesService, feature_category: :pages do
expect { service.execute }.to publish_event(Pages::PageDeployedEvent).with(expected_data)
end
- it 'creates pages_deployment and saves it in the metadata' do
- expect do
- expect(service.execute[:status]).to eq(:success)
- end.to change { project.pages_deployments.count }.by(1)
+ it 'creates pages_deployment' do
+ expect { expect(service.execute[:status]).to eq(:success) }
+ .to change { project.pages_deployments.count }
+ .by(1)
deployment = project.pages_deployments.last
@@ -148,7 +147,6 @@ RSpec.describe Projects::UpdatePagesService, feature_category: :pages do
expect(deployment.file).to be_present
expect(deployment.file_count).to eq(3)
expect(deployment.file_sha256).to eq(artifacts_archive.file_sha256)
- expect(project.pages_metadatum.reload.pages_deployment_id).to eq(deployment.id)
expect(deployment.ci_build_id).to eq(build.id)
expect(deployment.root_directory).to be_nil
end
@@ -157,11 +155,9 @@ RSpec.describe Projects::UpdatePagesService, feature_category: :pages do
project.pages_metadatum.destroy!
project.reload
- expect do
- expect(service.execute[:status]).to eq(:success)
- end.to change { project.pages_deployments.count }.by(1)
-
- expect(project.pages_metadatum.reload.pages_deployment).to eq(project.pages_deployments.last)
+ expect { expect(service.execute[:status]).to eq(:success) }
+ .to change { project.pages_deployments.count }
+ .by(1)
end
context 'when archive does not have pages directory' do
@@ -171,7 +167,10 @@ RSpec.describe Projects::UpdatePagesService, feature_category: :pages do
it 'returns an error' do
expect(service.execute[:status]).not_to eq(:success)
- expect(GenericCommitStatus.last.description).to eq("Error: You need to either include a `public/` folder in your artifacts, or specify which one to use for Pages using `publish` in `.gitlab-ci.yml`")
+ expect(GenericCommitStatus.last.description)
+ .to eq(
+ "Error: You need to either include a `public/` folder in your artifacts, " \
+ "or specify which one to use for Pages using `publish` in `.gitlab-ci.yml`")
end
end
@@ -196,7 +195,10 @@ RSpec.describe Projects::UpdatePagesService, feature_category: :pages do
it 'returns an error' do
expect(service.execute[:status]).not_to eq(:success)
- expect(GenericCommitStatus.last.description).to eq("Error: You need to either include a `public/` folder in your artifacts, or specify which one to use for Pages using `publish` in `.gitlab-ci.yml`")
+ expect(GenericCommitStatus.last.description)
+ .to eq(
+ "Error: You need to either include a `public/` folder in your artifacts, " \
+ "or specify which one to use for Pages using `publish` in `.gitlab-ci.yml`")
end
end
@@ -208,7 +210,10 @@ RSpec.describe Projects::UpdatePagesService, feature_category: :pages do
it 'returns an error' do
expect(service.execute[:status]).not_to eq(:success)
- expect(GenericCommitStatus.last.description).to eq("Error: You need to either include a `public/` folder in your artifacts, or specify which one to use for Pages using `publish` in `.gitlab-ci.yml`")
+ expect(GenericCommitStatus.last.description)
+ .to eq(
+ "Error: You need to either include a `public/` folder in your artifacts, " \
+ "or specify which one to use for Pages using `publish` in `.gitlab-ci.yml`")
end
end
end
@@ -223,7 +228,8 @@ RSpec.describe Projects::UpdatePagesService, feature_category: :pages do
expect(service.execute[:status]).not_to eq(:success)
- expect(GenericCommitStatus.last.description).to eq("pages site contains 3 file entries, while limit is set to 2")
+ expect(GenericCommitStatus.last.description)
+ .to eq("pages site contains 3 file entries, while limit is set to 2")
end
context 'when timeout happens by DNS error' do
@@ -240,13 +246,13 @@ RSpec.describe Projects::UpdatePagesService, feature_category: :pages do
deploy_status = GenericCommitStatus.last
expect(deploy_status).to be_failed
- expect(project.pages_metadatum).not_to be_deployed
+ expect(project.pages_deployed?).to eq(false)
end
end
context 'when missing artifacts metadata' do
before do
- expect(build).to receive(:artifacts_metadata?).and_return(false)
+ allow(build).to receive(:artifacts_metadata?).and_return(false)
end
it 'does not raise an error as failed job' do
@@ -256,7 +262,7 @@ RSpec.describe Projects::UpdatePagesService, feature_category: :pages do
deploy_status = GenericCommitStatus.last
expect(deploy_status).to be_failed
- expect(project.pages_metadatum).not_to be_deployed
+ expect(project.pages_deployed?).to eq(false)
end
end
@@ -275,10 +281,9 @@ RSpec.describe Projects::UpdatePagesService, feature_category: :pages do
end
end
- it 'creates a new pages deployment and mark it as deployed' do
- expect do
- expect(service.execute[:status]).to eq(:success)
- end.to change { project.pages_deployments.count }.by(1)
+ it 'creates a new pages deployment' do
+ expect { expect(service.execute[:status]).to eq(:success) }
+ .to change { project.pages_deployments.count }.by(1)
deployment = project.pages_deployments.last
expect(deployment.ci_build_id).to eq(build.id)
@@ -287,16 +292,12 @@ RSpec.describe Projects::UpdatePagesService, feature_category: :pages do
it_behaves_like 'old deployments'
context 'when newer deployment present' do
- before do
+ it 'fails with outdated reference message' do
new_pipeline = create(:ci_pipeline, project: project, sha: project.commit('HEAD').sha)
new_build = create(:ci_build, name: 'pages', pipeline: new_pipeline, ref: 'HEAD')
- new_deployment = create(:pages_deployment, ci_build: new_build, project: project)
- project.update_pages_deployment!(new_deployment)
- end
+ create(:pages_deployment, project: project, ci_build: new_build)
- it 'fails with outdated reference message' do
expect(service.execute[:status]).to eq(:error)
- expect(project.reload.pages_metadatum).not_to be_deployed
deploy_status = GenericCommitStatus.last
expect(deploy_status).to be_failed
@@ -308,16 +309,14 @@ RSpec.describe Projects::UpdatePagesService, feature_category: :pages do
it 'fails when uploaded deployment size is wrong' do
allow_next_instance_of(PagesDeployment) do |deployment|
allow(deployment)
- .to receive(:size)
- .and_return(file.size + 1)
+ .to receive(:file)
+ .and_return(instance_double(Pages::DeploymentUploader, size: file.size + 1))
end
expect(service.execute[:status]).not_to eq(:success)
- expect(GenericCommitStatus.last.description).to eq('The uploaded artifact size does not match the expected value')
- project.pages_metadatum.reload
- expect(project.pages_metadatum).not_to be_deployed
- expect(project.pages_metadatum.pages_deployment).to be_nil
+ expect(GenericCommitStatus.last.description)
+ .to eq('The uploaded artifact size does not match the expected value')
end
end
end
@@ -335,9 +334,8 @@ RSpec.describe Projects::UpdatePagesService, feature_category: :pages do
end
it 'fails with exception raised' do
- expect do
- service.execute
- end.to raise_error("Validation failed: File sha256 can't be blank")
+ expect { service.execute }
+ .to raise_error("Validation failed: File sha256 can't be blank")
end
end
diff --git a/spec/services/projects/update_repository_storage_service_spec.rb b/spec/services/projects/update_repository_storage_service_spec.rb
index d173d23a1d6..b81fc8bf633 100644
--- a/spec/services/projects/update_repository_storage_service_spec.rb
+++ b/spec/services/projects/update_repository_storage_service_spec.rb
@@ -79,6 +79,30 @@ RSpec.describe Projects::UpdateRepositoryStorageService, feature_category: :sour
end
end
+ context 'when touch raises an exception' do
+ let(:exception) { RuntimeError.new('Boom') }
+
+ it 'marks the storage move as failed and restores read-write access' do
+ allow(repository_storage_move).to receive(:container).and_return(project)
+
+ allow(project).to receive(:touch).and_wrap_original do
+ project.assign_attributes(updated_at: 1.second.ago)
+ raise exception
+ end
+
+ expect(project_repository_double).to receive(:replicate)
+ .with(project.repository.raw)
+ expect(project_repository_double).to receive(:checksum)
+ .and_return(checksum)
+
+ expect { subject.execute }.to raise_error(exception)
+ project.reload
+
+ expect(project).not_to be_repository_read_only
+ expect(repository_storage_move.reload).to be_failed
+ end
+ end
+
context 'when the filesystems are the same' do
before do
expect(Gitlab::GitalyClient).to receive(:filesystem_id).twice.and_return(SecureRandom.uuid)
diff --git a/spec/services/projects/update_statistics_service_spec.rb b/spec/services/projects/update_statistics_service_spec.rb
index f6565853460..5311b8daeb1 100644
--- a/spec/services/projects/update_statistics_service_spec.rb
+++ b/spec/services/projects/update_statistics_service_spec.rb
@@ -6,7 +6,7 @@ RSpec.describe Projects::UpdateStatisticsService, feature_category: :groups_and_
using RSpec::Parameterized::TableSyntax
let(:service) { described_class.new(project, nil, statistics: statistics) }
- let(:statistics) { %w(repository_size) }
+ let(:statistics) { %w[repository_size] }
describe '#execute' do
context 'with a non-existing project' do
@@ -23,13 +23,13 @@ RSpec.describe Projects::UpdateStatisticsService, feature_category: :groups_and_
let_it_be(:project) { create(:project) }
where(:statistics, :method_caches) do
- [] | %i(size recent_objects_size commit_count)
- ['repository_size'] | %i(size recent_objects_size)
- [:repository_size] | %i(size recent_objects_size)
+ [] | %i[size recent_objects_size commit_count]
+ ['repository_size'] | %i[size recent_objects_size]
+ [:repository_size] | %i[size recent_objects_size]
[:lfs_objects_size] | nil
[:commit_count] | [:commit_count]
- [:repository_size, :commit_count] | %i(size recent_objects_size commit_count)
- [:repository_size, :commit_count, :lfs_objects_size] | %i(size recent_objects_size commit_count)
+ [:repository_size, :commit_count] | %i[size recent_objects_size commit_count]
+ [:repository_size, :commit_count, :lfs_objects_size] | %i[size recent_objects_size commit_count]
end
with_them do
@@ -59,7 +59,7 @@ RSpec.describe Projects::UpdateStatisticsService, feature_category: :groups_and_
it 'invalidates and refreshes Wiki size' do
expect(project.statistics).to receive(:refresh!).with(only: statistics).and_call_original
- expect(project.wiki.repository).to receive(:expire_method_caches).with(%i(size)).and_call_original
+ expect(project.wiki.repository).to receive(:expire_method_caches).with(%i[size]).and_call_original
service.execute
end
diff --git a/spec/services/quick_actions/interpret_service_spec.rb b/spec/services/quick_actions/interpret_service_spec.rb
index 2c34d6a59be..1c9c6323e96 100644
--- a/spec/services/quick_actions/interpret_service_spec.rb
+++ b/spec/services/quick_actions/interpret_service_spec.rb
@@ -80,7 +80,7 @@ RSpec.describe QuickActions::InterpretService, feature_category: :team_planning
it 'returns the title message' do
_, _, message = service.execute(content, issuable)
- expect(message).to eq(%{Changed the title to "A brand new title".})
+ expect(message).to eq(%(Changed the title to "A brand new title".))
end
end
@@ -695,7 +695,7 @@ RSpec.describe QuickActions::InterpretService, feature_category: :team_planning
_, _, message = service.execute(content, issuable)
if tag_message.present?
- expect(message).to eq(%{Tagged this commit to #{tag_name} with "#{tag_message}".})
+ expect(message).to eq(%(Tagged this commit to #{tag_name} with "#{tag_message}".))
else
expect(message).to eq("Tagged this commit to #{tag_name}.")
end
@@ -1979,7 +1979,7 @@ RSpec.describe QuickActions::InterpretService, feature_category: :team_planning
context '/board_move command' do
let_it_be(:todo) { create(:label, project: project, title: 'To Do') }
let_it_be(:inreview) { create(:label, project: project, title: 'In Review') }
- let(:content) { %{/board_move ~"#{inreview.title}"} }
+ let(:content) { %(/board_move ~"#{inreview.title}") }
let_it_be(:board) { create(:board, project: project) }
let_it_be(:todo_list) { create(:list, board: board, label: todo) }
@@ -2043,14 +2043,14 @@ RSpec.describe QuickActions::InterpretService, feature_category: :team_planning
context 'if multiple labels are given' do
let(:issuable) { issue }
- let(:content) { %{/board_move ~"#{inreview.title}" ~"#{todo.title}"} }
+ let(:content) { %(/board_move ~"#{inreview.title}" ~"#{todo.title}") }
it_behaves_like 'failed command', 'Failed to move this issue because only a single label can be provided.'
end
context 'if the given label is not a list on the board' do
let(:issuable) { issue }
- let(:content) { %{/board_move ~"#{bug.title}"} }
+ let(:content) { %(/board_move ~"#{bug.title}") }
it_behaves_like 'failed command', 'Failed to move this issue because label was not found.'
end
@@ -2187,6 +2187,67 @@ RSpec.describe QuickActions::InterpretService, feature_category: :team_planning
end
end
+ context 'request_changes command' do
+ let(:merge_request) { create(:merge_request, source_project: project) }
+ let(:content) { '/request_changes' }
+
+ context "when `mr_request_changes` feature flag is disabled" do
+ before do
+ stub_feature_flags(mr_request_changes: false)
+ end
+
+ it 'does not call MergeRequests::UpdateReviewerStateService' do
+ expect(MergeRequests::UpdateReviewerStateService).not_to receive(:new)
+
+ service.execute(content, merge_request)
+ end
+ end
+
+ context "when the user is a reviewer" do
+ before do
+ create(:merge_request_reviewer, merge_request: merge_request, reviewer: current_user)
+ end
+
+ it 'calls MergeRequests::UpdateReviewerStateService with requested_changes' do
+ expect_next_instance_of(
+ MergeRequests::UpdateReviewerStateService,
+ project: project, current_user: current_user
+ ) do |service|
+ expect(service).to receive(:execute).with(merge_request, "requested_changes").and_return({ status: :success })
+ end
+
+ _, _, message = service.execute(content, merge_request)
+
+ expect(message).to eq('Changes requested to the current merge request.')
+ end
+
+ it 'returns error message from MergeRequests::UpdateReviewerStateService' do
+ expect_next_instance_of(
+ MergeRequests::UpdateReviewerStateService,
+ project: project, current_user: current_user
+ ) do |service|
+ expect(service).to receive(:execute).with(merge_request, "requested_changes").and_return({ status: :error, message: 'Error' })
+ end
+
+ _, _, message = service.execute(content, merge_request)
+
+ expect(message).to eq('Error')
+ end
+ end
+
+ context "when the user is not a reviewer" do
+ it 'does not call MergeRequests::UpdateReviewerStateService' do
+ expect(MergeRequests::UpdateReviewerStateService).not_to receive(:new)
+
+ service.execute(content, merge_request)
+ end
+ end
+
+ it_behaves_like 'approve command unavailable' do
+ let(:issuable) { issue }
+ end
+ end
+
it_behaves_like 'issues link quick action', :relate do
let(:user) { developer }
end
@@ -2422,6 +2483,17 @@ RSpec.describe QuickActions::InterpretService, feature_category: :team_planning
expect(merge_request.approved_by_users).to be_empty
end
+ it 'calls MergeRequests::UpdateReviewerStateService' do
+ expect_next_instance_of(
+ MergeRequests::UpdateReviewerStateService,
+ project: project, current_user: current_user
+ ) do |service|
+ expect(service).to receive(:execute).with(merge_request, "unreviewed")
+ end
+
+ service.execute(content, merge_request)
+ end
+
context "when the user can't unapprove" do
before do
project.team.truncate
diff --git a/spec/services/releases/create_service_spec.rb b/spec/services/releases/create_service_spec.rb
index 0170c3abcaf..3504f00412c 100644
--- a/spec/services/releases/create_service_spec.rb
+++ b/spec/services/releases/create_service_spec.rb
@@ -56,21 +56,25 @@ RSpec.describe Releases::CreateService, feature_category: :continuous_integratio
end
context 'when project is a catalog resource' do
- let(:ref) { 'master' }
+ let(:project) { create(:project, :catalog_resource_with_components, create_tag: 'final') }
let!(:ci_catalog_resource) { create(:ci_catalog_resource, project: project) }
+ let(:ref) { 'master' }
context 'and it is valid' do
- let_it_be(:project) { create(:project, :repository, description: 'our components') }
-
it_behaves_like 'a successful release creation'
end
- context 'and it is invalid' do
+ context 'and it is an invalid resource' do
+ let_it_be(:project) { create(:project, :repository) }
+
it 'raises an error and does not update the release' do
result = service.execute
expect(result[:status]).to eq(:error)
- expect(result[:message]).to eq('Project must have a description')
+ expect(result[:http_status]).to eq(422)
+ expect(result[:message]).to eq(
+ 'Project must have a description, ' \
+ 'Project must contain components. Ensure you are using the correct directory structure')
end
end
end
@@ -104,6 +108,7 @@ RSpec.describe Releases::CreateService, feature_category: :continuous_integratio
result = service.execute
expect(result[:status]).to eq(:error)
+ expect(result[:http_status]).to eq(403)
end
end
@@ -139,6 +144,7 @@ RSpec.describe Releases::CreateService, feature_category: :continuous_integratio
it 'raises an error and does not update the release' do
result = service.execute
expect(result[:status]).to eq(:error)
+ expect(result[:http_status]).to eq(409)
expect(project.releases.find_by(tag: tag_name).description).to eq(description)
end
end
@@ -150,6 +156,7 @@ RSpec.describe Releases::CreateService, feature_category: :continuous_integratio
result = service.execute
expect(result[:status]).to eq(:error)
+ expect(result[:http_status]).to eq(400)
expect(result[:message]).to eq("Milestone(s) not found: #{inexistent_milestone_tag}")
end
@@ -159,6 +166,7 @@ RSpec.describe Releases::CreateService, feature_category: :continuous_integratio
result = service.execute
expect(result[:status]).to eq(:error)
+ expect(result[:http_status]).to eq(400)
expect(result[:message]).to eq("Milestone id(s) not found: #{inexistent_milestone_id}")
end
end
@@ -244,6 +252,7 @@ RSpec.describe Releases::CreateService, feature_category: :continuous_integratio
result = service.execute
expect(result[:status]).to eq(:error)
+ expect(result[:http_status]).to eq(400)
expect(result[:message]).to eq("Milestone(s) not found: #{inexistent_title}")
end
@@ -260,6 +269,7 @@ RSpec.describe Releases::CreateService, feature_category: :continuous_integratio
result = service.execute
expect(result[:status]).to eq(:error)
+ expect(result[:http_status]).to eq(400)
expect(result[:message]).to eq("Milestone id(s) not found: #{non_existing_record_id}")
end
end
diff --git a/spec/services/service_desk/custom_email_verifications/update_service_spec.rb b/spec/services/service_desk/custom_email_verifications/update_service_spec.rb
index d882cd8635a..f87952d1d0e 100644
--- a/spec/services/service_desk/custom_email_verifications/update_service_spec.rb
+++ b/spec/services/service_desk/custom_email_verifications/update_service_spec.rb
@@ -22,6 +22,7 @@ RSpec.describe ServiceDesk::CustomEmailVerifications::UpdateService, feature_cat
end
let(:expected_error_message) { error_parameter_missing }
+ let(:expected_custom_email_enabled) { false }
let(:logger_params) { { category: 'custom_email_verification' } }
before do
@@ -30,7 +31,7 @@ RSpec.describe ServiceDesk::CustomEmailVerifications::UpdateService, feature_cat
end
shared_examples 'a failing verification process' do |expected_error_identifier|
- it 'refuses to verify and sends result emails' do
+ it 'refuses to verify and sends result emails', :aggregate_failures do
expect(Notify).to receive(:service_desk_verification_result_email).twice
expect(Gitlab::AppLogger).to receive(:info).with(logger_params.merge(
@@ -52,7 +53,7 @@ RSpec.describe ServiceDesk::CustomEmailVerifications::UpdateService, feature_cat
end
shared_examples 'an early exit from the verification process' do |expected_state|
- it 'exits early' do
+ it 'exits early', :aggregate_failures do
expect(Notify).to receive(:service_desk_verification_result_email).exactly(0).times
expect(Gitlab::AppLogger).to receive(:warn).with(logger_params.merge(
@@ -65,7 +66,7 @@ RSpec.describe ServiceDesk::CustomEmailVerifications::UpdateService, feature_cat
verification.reset
expect(response).to be_error
- expect(settings).not_to be_custom_email_enabled
+ expect(settings.custom_email_enabled).to eq expected_custom_email_enabled
expect(verification.state).to eq expected_state
end
end
@@ -179,6 +180,26 @@ RSpec.describe ServiceDesk::CustomEmailVerifications::UpdateService, feature_cat
it_behaves_like 'a failing verification process', 'mail_not_received_within_timeframe'
end
+
+ context 'when already verified' do
+ let(:expected_error_message) { error_already_finished }
+
+ before do
+ verification.mark_as_finished!
+ end
+
+ it_behaves_like 'an early exit from the verification process', 'finished'
+
+ context 'when enabled' do
+ let(:expected_custom_email_enabled) { true }
+
+ before do
+ settings.update!(custom_email_enabled: true)
+ end
+
+ it_behaves_like 'an early exit from the verification process', 'finished'
+ end
+ end
end
end
end
diff --git a/spec/services/service_desk/custom_emails/create_service_spec.rb b/spec/services/service_desk/custom_emails/create_service_spec.rb
index 2029c9a0c3f..e165131bcf9 100644
--- a/spec/services/service_desk/custom_emails/create_service_spec.rb
+++ b/spec/services/service_desk/custom_emails/create_service_spec.rb
@@ -156,7 +156,7 @@ RSpec.describe ServiceDesk::CustomEmails::CreateService, feature_category: :serv
}
end
- it 'creates all records returns a successful response' do
+ it 'creates all records and returns a successful response' do
# Because we also log in ServiceDesk::CustomEmailVerifications::CreateService
expect(Gitlab::AppLogger).to receive(:info).with({ category: 'custom_email_verification' }).once
expect(Gitlab::AppLogger).to receive(:info).with(logger_params).once
@@ -174,7 +174,8 @@ RSpec.describe ServiceDesk::CustomEmails::CreateService, feature_category: :serv
smtp_address: params[:smtp_address],
smtp_port: params[:smtp_port].to_i,
smtp_username: params[:smtp_username],
- smtp_password: params[:smtp_password]
+ smtp_password: params[:smtp_password],
+ smtp_authentication: nil
)
expect(project.service_desk_custom_email_verification).to have_attributes(
state: 'started',
@@ -183,6 +184,30 @@ RSpec.describe ServiceDesk::CustomEmails::CreateService, feature_category: :serv
)
end
+ context 'with optional smtp_authentication parameter' do
+ before do
+ params[:smtp_authentication] = 'login'
+ end
+
+ it 'sets authentication and returns a successful response' do
+ response = service.execute
+ project.reset
+
+ expect(response).to be_success
+ expect(project.service_desk_custom_email_credential.smtp_authentication).to eq 'login'
+ end
+
+ context 'with unsupported value' do
+ let(:expected_error_message) { error_cannot_create_custom_email }
+
+ before do
+ params[:smtp_authentication] = 'unsupported'
+ end
+
+ it_behaves_like 'a failing service that does not create records'
+ end
+ end
+
context 'when custom email aready exists' do
let!(:settings) { create(:service_desk_setting, project: project, custom_email: 'user@example.com') }
let!(:credential) { create(:service_desk_custom_email_credential, project: project) }
diff --git a/spec/services/service_desk_settings/update_service_spec.rb b/spec/services/service_desk_settings/update_service_spec.rb
index 27978225bcf..a9e54012075 100644
--- a/spec/services/service_desk_settings/update_service_spec.rb
+++ b/spec/services/service_desk_settings/update_service_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-RSpec.describe ServiceDeskSettings::UpdateService, feature_category: :service_desk do
+RSpec.describe ServiceDeskSettings::UpdateService, :aggregate_failures, feature_category: :service_desk do
describe '#execute' do
let_it_be(:settings) do
create(:service_desk_setting, outgoing_name: 'original name', custom_email: 'user@example.com')
@@ -12,14 +12,17 @@ RSpec.describe ServiceDeskSettings::UpdateService, feature_category: :service_de
let_it_be(:user) { create(:user) }
context 'with valid params' do
- let(:params) { { outgoing_name: 'some name', project_key: 'foo' } }
+ let(:params) { { outgoing_name: 'some name', project_key: 'foo', add_external_participants_from_cc: true } }
it 'updates service desk settings' do
response = described_class.new(settings.project, user, params).execute
expect(response).to be_success
- expect(settings.reload.outgoing_name).to eq 'some name'
- expect(settings.reload.project_key).to eq 'foo'
+ expect(settings.reset).to have_attributes(
+ outgoing_name: 'some name',
+ project_key: 'foo',
+ add_external_participants_from_cc: true
+ )
end
context 'with custom email verification in finished state' do
@@ -39,6 +42,23 @@ RSpec.describe ServiceDeskSettings::UpdateService, feature_category: :service_de
expect(Gitlab::AppLogger).to have_received(:info).with({ category: 'custom_email' })
end
end
+
+ context 'when issue_email_participants feature flag is disabled' do
+ before do
+ stub_feature_flags(issue_email_participants: false)
+ end
+
+ it 'updates service desk setting but not add_external_participants_from_cc value' do
+ response = described_class.new(settings.project, user, params).execute
+
+ expect(response).to be_success
+ expect(settings.reset).to have_attributes(
+ outgoing_name: 'some name',
+ project_key: 'foo',
+ add_external_participants_from_cc: false
+ )
+ end
+ end
end
context 'when project_key is an empty string' do
diff --git a/spec/services/spam/spam_action_service_spec.rb b/spec/services/spam/spam_action_service_spec.rb
index 4133609d9ae..d8fd09ebd07 100644
--- a/spec/services/spam/spam_action_service_spec.rb
+++ b/spec/services/spam/spam_action_service_spec.rb
@@ -85,6 +85,26 @@ RSpec.describe Spam::SpamActionService, feature_category: :instance_resiliency d
end
end
+ shared_examples 'calls SpamAbuseEventsWorker with correct arguments' do
+ let(:params) do
+ {
+ user_id: user.id,
+ title: target.title,
+ description: target.spam_description,
+ source_ip: fake_ip,
+ user_agent: fake_user_agent,
+ noteable_type: target_type,
+ verdict: verdict
+ }
+ end
+
+ it do
+ expect(::Abuse::SpamAbuseEventsWorker).to receive(:perform_async).with(params)
+
+ subject
+ end
+ end
+
shared_examples 'execute spam action service' do |target_type|
let(:fake_captcha_verification_service) { double(:captcha_verification_service) }
let(:fake_verdict_service) { double(:spam_verdict_service) }
@@ -161,6 +181,12 @@ RSpec.describe Spam::SpamActionService, feature_category: :instance_resiliency d
it 'does not create a spam log' do
expect { subject }.not_to change(SpamLog, :count)
end
+
+ it 'does not call SpamAbuseEventsWorker' do
+ expect(::Abuse::SpamAbuseEventsWorker).not_to receive(:perform_async)
+
+ subject
+ end
end
context 'when spammable attributes have changed' do
@@ -213,6 +239,11 @@ RSpec.describe Spam::SpamActionService, feature_category: :instance_resiliency d
it_behaves_like 'creates a spam log', target_type
+ it_behaves_like 'calls SpamAbuseEventsWorker with correct arguments' do
+ let(:verdict) { DISALLOW }
+ let(:target_type) { target_type }
+ end
+
it 'marks as spam' do
response = subject
@@ -231,6 +262,11 @@ RSpec.describe Spam::SpamActionService, feature_category: :instance_resiliency d
it_behaves_like 'creates a spam log', target_type
+ it_behaves_like 'calls SpamAbuseEventsWorker with correct arguments' do
+ let(:verdict) { BLOCK_USER }
+ let(:target_type) { target_type }
+ end
+
it 'marks as spam' do
response = subject
@@ -254,6 +290,11 @@ RSpec.describe Spam::SpamActionService, feature_category: :instance_resiliency d
it_behaves_like 'creates a spam log', target_type
+ it_behaves_like 'calls SpamAbuseEventsWorker with correct arguments' do
+ let(:verdict) { CONDITIONAL_ALLOW }
+ let(:target_type) { target_type }
+ end
+
it 'does not mark as spam' do
response = subject
@@ -276,6 +317,12 @@ RSpec.describe Spam::SpamActionService, feature_category: :instance_resiliency d
it_behaves_like 'creates a spam log', target_type
+ it 'does not call SpamAbuseEventsWorker' do
+ expect(::Abuse::SpamAbuseEventsWorker).not_to receive(:perform_async)
+
+ subject
+ end
+
it 'does not mark as spam' do
response = subject
@@ -300,6 +347,12 @@ RSpec.describe Spam::SpamActionService, feature_category: :instance_resiliency d
expect { subject }.not_to change(SpamLog, :count)
end
+ it 'does not call SpamAbuseEventsWorker' do
+ expect(::Abuse::SpamAbuseEventsWorker).not_to receive(:perform_async)
+
+ subject
+ end
+
it 'clears spam flags' do
expect(target).to receive(:clear_spam_flags!)
@@ -316,6 +369,12 @@ RSpec.describe Spam::SpamActionService, feature_category: :instance_resiliency d
expect { subject }.not_to change(SpamLog, :count)
end
+ it 'does not call SpamAbuseEventsWorker' do
+ expect(::Abuse::SpamAbuseEventsWorker).not_to receive(:perform_async)
+
+ subject
+ end
+
it 'clears spam flags' do
expect(target).to receive(:clear_spam_flags!)
diff --git a/spec/services/system_notes/issuables_service_spec.rb b/spec/services/system_notes/issuables_service_spec.rb
index bcca1ed0b23..ca6feb6fde2 100644
--- a/spec/services/system_notes/issuables_service_spec.rb
+++ b/spec/services/system_notes/issuables_service_spec.rb
@@ -784,7 +784,7 @@ RSpec.describe ::SystemNotes::IssuablesService, feature_category: :team_planning
service = described_class.new(noteable: issuable, author: author)
expect(service.discussion_lock.note)
- .to eq("unlocked this #{type.to_s.titleize.downcase}")
+ .to eq("unlocked the discussion in this #{type.to_s.titleize.downcase}")
end
end
end
@@ -804,7 +804,7 @@ RSpec.describe ::SystemNotes::IssuablesService, feature_category: :team_planning
service = described_class.new(noteable: issuable, author: author)
expect(service.discussion_lock.note)
- .to eq("locked this #{type.to_s.titleize.downcase}")
+ .to eq("locked the discussion in this #{type.to_s.titleize.downcase}")
end
end
end
diff --git a/spec/services/upload_service_spec.rb b/spec/services/upload_service_spec.rb
index 518d12d5b41..4a8cd46172d 100644
--- a/spec/services/upload_service_spec.rb
+++ b/spec/services/upload_service_spec.rb
@@ -81,7 +81,7 @@ RSpec.describe UploadService, feature_category: :shared do
it 'allows the upload' do
service.override_max_attachment_size = 101.megabytes
- expect(subject.keys).to eq(%i(alt url markdown))
+ expect(subject.keys).to eq(%i[alt url markdown])
end
it 'disallows the upload' do
diff --git a/spec/services/users/refresh_authorized_projects_service_spec.rb b/spec/services/users/refresh_authorized_projects_service_spec.rb
index b36152f81c3..3d88618711b 100644
--- a/spec/services/users/refresh_authorized_projects_service_spec.rb
+++ b/spec/services/users/refresh_authorized_projects_service_spec.rb
@@ -98,6 +98,13 @@ RSpec.describe Users::RefreshAuthorizedProjectsService, feature_category: :user_
service.execute_without_lease
end
+ it 'updates project_authorizations_recalculated_at', :freeze_time do
+ default_date = Time.zone.local('2010')
+ expect do
+ service.execute_without_lease
+ end.to change { user.project_authorizations_recalculated_at }.from(default_date).to(Time.zone.now)
+ end
+
it 'returns a User' do
expect(service.execute_without_lease).to be_an_instance_of(User)
end
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 4e23b51cae2..e1c5b30115d 100644
--- a/spec/services/users/upsert_credit_card_validation_service_spec.rb
+++ b/spec/services/users/upsert_credit_card_validation_service_spec.rb
@@ -3,20 +3,29 @@
require 'spec_helper'
RSpec.describe Users::UpsertCreditCardValidationService, feature_category: :user_profile do
+ include CryptoHelpers
+
let_it_be(:user) { create(:user) }
let(:user_id) { user.id }
- let(:credit_card_validated_time) { Time.utc(2020, 1, 1) }
+
+ let(:network) { 'American Express' }
+ let(:holder_name) { 'John Smith' }
+ let(:last_digits) { '1111' }
let(:expiration_year) { Date.today.year + 10 }
+ let(:expiration_month) { 1 }
+ let(:expiration_date) { Date.new(expiration_year, expiration_month, -1) }
+ let(:credit_card_validated_at) { Time.utc(2020, 1, 1) }
+
let(:params) do
{
user_id: user_id,
- credit_card_validated_at: credit_card_validated_time,
+ credit_card_validated_at: credit_card_validated_at,
credit_card_expiration_year: expiration_year,
- credit_card_expiration_month: 1,
- credit_card_holder_name: 'John Smith',
- credit_card_type: 'AmericanExpress',
- credit_card_mask_number: '1111'
+ credit_card_expiration_month: expiration_month,
+ credit_card_holder_name: holder_name,
+ credit_card_type: network,
+ credit_card_mask_number: last_digits
}
end
@@ -25,82 +34,97 @@ RSpec.describe Users::UpsertCreditCardValidationService, feature_category: :user
context 'successfully set credit card validation record for the user' do
context 'when user does not have credit card validation record' do
- it 'creates the credit card validation and returns a success' do
+ it 'creates the credit card validation and returns a success', :aggregate_failures do
expect(user.credit_card_validated_at).to be nil
- result = service.execute
+ service_result = service.execute
- expect(result.status).to eq(:success)
+ expect(service_result.status).to eq(:success)
+ expect(service_result.message).to eq(_('Credit card validation record saved'))
user.reload
expect(user.credit_card_validation).to have_attributes(
- credit_card_validated_at: credit_card_validated_time,
- network: 'AmericanExpress',
- holder_name: 'John Smith',
- last_digits: 1111,
- expiration_date: Date.new(expiration_year, 1, 31)
+ credit_card_validated_at: credit_card_validated_at,
+ network_hash: sha256(network.downcase),
+ holder_name_hash: sha256(holder_name.downcase),
+ last_digits_hash: sha256(last_digits),
+ expiration_date_hash: sha256(expiration_date.to_s)
)
end
end
context 'when user has credit card validation record' do
- let(:old_time) { Time.utc(1999, 2, 2) }
+ let(:previous_credit_card_validated_at) { Time.utc(1999, 2, 2) }
before do
- create(:credit_card_validation, user: user, credit_card_validated_at: old_time)
+ create(:credit_card_validation, user: user, credit_card_validated_at: previous_credit_card_validated_at)
end
- it 'updates the credit card validation and returns a success' do
- expect(user.credit_card_validated_at).to eq(old_time)
+ it 'updates the credit card validation record and returns a success', :aggregate_failures do
+ expect(user.credit_card_validated_at).to eq(previous_credit_card_validated_at)
+
+ service_result = service.execute
- result = service.execute
+ expect(service_result.status).to eq(:success)
+ expect(service_result.message).to eq(_('Credit card validation record saved'))
- expect(result.status).to eq(:success)
- expect(user.reload.credit_card_validated_at).to eq(credit_card_validated_time)
+ user.reload
+
+ expect(user.credit_card_validated_at).to eq(credit_card_validated_at)
end
end
end
shared_examples 'returns an error without tracking the exception' do
- it do
+ it 'does not send an exception to Gitlab::ErrorTracking' do
expect(Gitlab::ErrorTracking).not_to receive(:track_exception)
- result = service.execute
+ service.execute
+ end
+
+ it 'returns an error', :aggregate_failures do
+ service_result = service.execute
- expect(result.status).to eq(:error)
+ expect(service_result.status).to eq(:error)
+ expect(service_result.message).to eq(_('Error saving credit card validation record'))
end
end
- shared_examples 'returns an error, tracking the exception' do
- it do
+ shared_examples 'returns an error and tracks the exception' do
+ it 'sends an exception to Gitlab::ErrorTracking' do
expect(Gitlab::ErrorTracking).to receive(:track_exception)
- result = service.execute
+ service.execute
+ end
+
+ it 'returns an error', :aggregate_failures do
+ service_result = service.execute
- expect(result.status).to eq(:error)
+ expect(service_result.status).to eq(:error)
+ expect(service_result.message).to eq(_('Error saving credit card validation record'))
end
end
- context 'when user id does not exist' do
+ context 'when the user_id does not exist' do
let(:user_id) { non_existing_record_id }
it_behaves_like 'returns an error without tracking the exception'
end
- context 'when missing credit_card_validated_at' do
+ context 'when the request is missing the credit_card_validated_at field' do
let(:params) { { user_id: user_id } }
- it_behaves_like 'returns an error, tracking the exception'
+ it_behaves_like 'returns an error and tracks the exception'
end
- context 'when missing user id' do
- let(:params) { { credit_card_validated_at: credit_card_validated_time } }
+ context 'when the request is missing the user_id field' do
+ let(:params) { { credit_card_validated_at: credit_card_validated_at } }
- it_behaves_like 'returns an error, tracking the exception'
+ it_behaves_like 'returns an error and tracks the exception'
end
- context 'when unexpected exception happen' do
+ context 'when there is an unexpected error' do
let(:exception) { StandardError.new }
before do
@@ -109,22 +133,7 @@ RSpec.describe Users::UpsertCreditCardValidationService, feature_category: :user
end
end
- it 'tracks the exception and returns an error' do
- logged_params = {
- credit_card_validated_at: credit_card_validated_time,
- expiration_date: Date.new(expiration_year, 1, 31),
- holder_name: "John Smith",
- last_digits: 1111,
- network: "AmericanExpress",
- user_id: user_id
- }
-
- expect(Gitlab::ErrorTracking).to receive(:track_exception).with(exception, class: described_class.to_s, params: logged_params)
-
- result = service.execute
-
- expect(result.status).to eq(:error)
- end
+ it_behaves_like 'returns an error and tracks the exception'
end
end
end
diff --git a/spec/services/vs_code/settings/delete_service_spec.rb b/spec/services/vs_code/settings/delete_service_spec.rb
new file mode 100644
index 00000000000..fd19c01569f
--- /dev/null
+++ b/spec/services/vs_code/settings/delete_service_spec.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe VsCode::Settings::DeleteService, feature_category: :web_ide do
+ describe '#execute' do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:other_user) { create(:user) }
+ let_it_be(:setting_one) { create(:vscode_setting, user: user) }
+ let_it_be(:setting_two) { create(:vscode_setting, setting_type: 'extensions', user: user) }
+ let_it_be(:setting_three) { create(:vscode_setting, setting_type: 'extensions', user: other_user) }
+
+ subject { described_class.new(current_user: user).execute }
+
+ it 'deletes all vscode_settings belonging to the current user' do
+ expect { subject }
+ .to change { User.find(user.id).vscode_settings.count }.from(2).to(0)
+ .and not_change { User.find(other_user.id).vscode_settings.count }
+ end
+ end
+end
diff --git a/spec/services/web_hook_service_spec.rb b/spec/services/web_hook_service_spec.rb
index 89346353db2..c33273348f6 100644
--- a/spec/services/web_hook_service_spec.rb
+++ b/spec/services/web_hook_service_spec.rb
@@ -13,6 +13,8 @@ RSpec.describe WebHookService, :request_store, :clean_gitlab_redis_shared_state,
{ before: 'oldrev', after: 'newrev', ref: 'ref' }
end
+ let(:serialized_data) { data.deep_stringify_keys }
+
let(:service_instance) { described_class.new(project_hook, data, :push_hooks) }
describe '#initialize' do
@@ -426,9 +428,9 @@ RSpec.describe WebHookService, :request_store, :clean_gitlab_redis_shared_state,
expect(WebHooks::LogExecutionWorker).to receive(:perform_async)
.with(
project_hook.id,
- hash_including(default_log_data),
- :ok,
- nil
+ hash_including(default_log_data.deep_stringify_keys),
+ 'ok',
+ ''
)
service_instance.execute
@@ -456,10 +458,10 @@ RSpec.describe WebHookService, :request_store, :clean_gitlab_redis_shared_state,
default_log_data.merge(
response_body: 'Bad request',
response_status: 400
- )
+ ).deep_stringify_keys
),
- :failed,
- nil
+ 'failed',
+ ''
)
service_instance.execute
@@ -480,10 +482,10 @@ RSpec.describe WebHookService, :request_store, :clean_gitlab_redis_shared_state,
response_body: '',
response_status: 'internal error',
internal_error_message: 'Some HTTP Post error'
- )
+ ).deep_stringify_keys
),
- :error,
- nil
+ 'error',
+ ''
)
service_instance.execute
@@ -499,9 +501,9 @@ RSpec.describe WebHookService, :request_store, :clean_gitlab_redis_shared_state,
expect(WebHooks::LogExecutionWorker).to receive(:perform_async)
.with(
project_hook.id,
- hash_including(default_log_data.merge(response_body: '')),
- :ok,
- nil
+ hash_including(default_log_data.merge(response_body: '').deep_stringify_keys),
+ 'ok',
+ ''
)
service_instance.execute
@@ -520,9 +522,9 @@ RSpec.describe WebHookService, :request_store, :clean_gitlab_redis_shared_state,
expect(WebHooks::LogExecutionWorker).to receive(:perform_async)
.with(
project_hook.id,
- hash_including(default_log_data.merge(response_body: stripped_body)),
- :ok,
- nil
+ hash_including(default_log_data.merge(response_body: stripped_body).deep_stringify_keys),
+ 'ok',
+ ''
)
service_instance.execute
@@ -553,9 +555,9 @@ RSpec.describe WebHookService, :request_store, :clean_gitlab_redis_shared_state,
expect(WebHooks::LogExecutionWorker).to receive(:perform_async)
.with(
project_hook.id,
- hash_including(default_log_data.merge(response_headers: expected_response_headers)),
- :ok,
- nil
+ hash_including(default_log_data.merge(response_headers: expected_response_headers).deep_stringify_keys),
+ 'ok',
+ ''
)
service_instance.execute
@@ -578,9 +580,9 @@ RSpec.describe WebHookService, :request_store, :clean_gitlab_redis_shared_state,
expect(WebHooks::LogExecutionWorker).to receive(:perform_async)
.with(
project_hook.id,
- hash_including(default_log_data.merge(response_headers: expected_response_headers)),
- :ok,
- nil
+ hash_including(default_log_data.merge(response_headers: expected_response_headers).deep_stringify_keys),
+ 'ok',
+ ''
)
service_instance.execute
@@ -596,9 +598,9 @@ RSpec.describe WebHookService, :request_store, :clean_gitlab_redis_shared_state,
expect(WebHooks::LogExecutionWorker).to receive(:perform_async)
.with(
project_hook.id,
- hash_including(default_log_data),
- :ok,
- nil
+ hash_including(default_log_data.deep_stringify_keys),
+ 'ok',
+ ''
)
.and_raise(
Gitlab::SidekiqMiddleware::SizeLimiter::ExceedLimitError.new(WebHooks::LogExecutionWorker, 100, 50)
@@ -607,9 +609,11 @@ RSpec.describe WebHookService, :request_store, :clean_gitlab_redis_shared_state,
expect(WebHooks::LogExecutionWorker).to receive(:perform_async)
.with(
project_hook.id,
- hash_including(default_log_data.merge(request_data: WebHookLog::OVERSIZE_REQUEST_DATA)),
- :ok,
- nil
+ hash_including(default_log_data.merge(
+ request_data: WebHookLog::OVERSIZE_REQUEST_DATA
+ ).deep_stringify_keys),
+ 'ok',
+ ''
)
.and_call_original
.ordered
@@ -636,7 +640,9 @@ RSpec.describe WebHookService, :request_store, :clean_gitlab_redis_shared_state,
describe '#async_execute' do
def expect_to_perform_worker(hook)
- expect(WebHookWorker).to receive(:perform_async).with(hook.id, data, 'push_hooks', an_instance_of(Hash))
+ expect(WebHookWorker).to receive(:perform_async).with(
+ hook.id, serialized_data, 'push_hooks', an_instance_of(Hash)
+ )
end
def expect_to_rate_limit(hook, threshold:, throttled: false)
diff --git a/spec/sidekiq_cluster/sidekiq_cluster_spec.rb b/spec/sidekiq_cluster/sidekiq_cluster_spec.rb
index 25a600405fe..ec5e5d85eeb 100644
--- a/spec/sidekiq_cluster/sidekiq_cluster_spec.rb
+++ b/spec/sidekiq_cluster/sidekiq_cluster_spec.rb
@@ -31,7 +31,7 @@ RSpec.describe Gitlab::SidekiqCluster do # rubocop:disable RSpec/FilePath
).and_return(2)
expect(Process).to receive(:detach).ordered.with(2)
- described_class.start([%w(foo), %w(bar baz)], env: :production, directory: 'foo/bar', max_concurrency: 20, min_concurrency: 10)
+ described_class.start([%w[foo], %w[bar baz]], env: :production, directory: 'foo/bar', max_concurrency: 20, min_concurrency: 10)
end
it 'starts Sidekiq with the given queues and sensible default options' do
@@ -45,10 +45,10 @@ RSpec.describe Gitlab::SidekiqCluster do # rubocop:disable RSpec/FilePath
dryrun: false
}
- expect(described_class).to receive(:start_sidekiq).ordered.with(%w(foo bar baz), expected_options)
- expect(described_class).to receive(:start_sidekiq).ordered.with(%w(solo), expected_options)
+ expect(described_class).to receive(:start_sidekiq).ordered.with(%w[foo bar baz], expected_options)
+ expect(described_class).to receive(:start_sidekiq).ordered.with(%w[solo], expected_options)
- described_class.start([%w(foo bar baz), %w(solo)])
+ described_class.start([%w[foo bar baz], %w[solo]])
end
end
@@ -67,7 +67,7 @@ RSpec.describe Gitlab::SidekiqCluster do # rubocop:disable RSpec/FilePath
allow(Process).to receive(:spawn).and_return(1)
allow(Process).to receive(:detach).with(1).and_return(waiter_thread)
- expect(described_class.start_sidekiq(%w(foo), **options)).to eq(waiter_thread)
+ expect(described_class.start_sidekiq(%w[foo], **options)).to eq(waiter_thread)
end
it 'handles duplicate queue names' do
@@ -77,7 +77,7 @@ RSpec.describe Gitlab::SidekiqCluster do # rubocop:disable RSpec/FilePath
.and_return(1)
allow(Process).to receive(:detach).with(1).and_return(waiter_thread)
- expect(described_class.start_sidekiq(%w(foo foo bar baz), **options)).to eq(waiter_thread)
+ expect(described_class.start_sidekiq(%w[foo foo bar baz], **options)).to eq(waiter_thread)
end
it 'runs the sidekiq process in a new process group' do
@@ -87,15 +87,15 @@ RSpec.describe Gitlab::SidekiqCluster do # rubocop:disable RSpec/FilePath
.and_return(1)
allow(Process).to receive(:detach).with(1).and_return(waiter_thread)
- expect(described_class.start_sidekiq(%w(foo bar baz), **options)).to eq(waiter_thread)
+ expect(described_class.start_sidekiq(%w[foo bar baz], **options)).to eq(waiter_thread)
end
end
describe '.count_by_queue' do
it 'tallies the queue counts' do
- queues = [%w(foo), %w(bar baz), %w(foo)]
+ queues = [%w[foo], %w[bar baz], %w[foo]]
- expect(described_class.count_by_queue(queues)).to eq(%w(foo) => 2, %w(bar baz) => 1)
+ expect(described_class.count_by_queue(queues)).to eq(%w[foo] => 2, %w[bar baz] => 1)
end
end
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index 02db905b8b1..2dd4e92eee9 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -24,7 +24,6 @@ CrystalballEnv.start!
ENV["RAILS_ENV"] = 'test'
ENV["IN_MEMORY_APPLICATION_SETTINGS"] = 'true'
ENV["RSPEC_ALLOW_INVALID_URLS"] = 'true'
-ENV['USE_CI_BUILDS_ROUTING_TABLE'] = 'true'
require_relative '../config/environment'
@@ -302,13 +301,6 @@ RSpec.configure do |config|
# https://gitlab.com/gitlab-org/gitlab/-/issues/385453
stub_feature_flags(vscode_web_ide: false)
- enable_rugged = example.metadata[:enable_rugged].present?
-
- # Disable Rugged features by default
- Gitlab::Git::RuggedImpl::Repository::FEATURE_FLAGS.each do |flag|
- stub_feature_flags(flag => enable_rugged)
- end
-
# Disable `main_branch_over_master` as we migrate
# from `master` to `main` accross our codebase.
# It's done in order to preserve the concistency in tests
@@ -336,8 +328,6 @@ RSpec.configure do |config|
stub_feature_flags(clickhouse_data_collection: false)
stub_feature_flags(vite: false)
-
- allow(Gitlab::GitalyClient).to receive(:can_use_disk?).and_return(enable_rugged)
else
unstub_all_feature_flags
end
@@ -394,11 +384,6 @@ RSpec.configure do |config|
::Gitlab::SafeRequestStore.ensure_request_store { example.run }
end
- config.around(:example, :enable_rugged) do |example|
- # Skip tests that need rugged when using praefect DB.
- example.run unless GitalySetup.praefect_with_db?
- end
-
config.around(:example, :yaml_processor_feature_flag_corectness) do |example|
::Gitlab::Ci::YamlProcessor::FeatureFlags.ensure_correct_usage do
example.run
diff --git a/spec/support/atlassian/jira_connect/schemata.rb b/spec/support/atlassian/jira_connect/schemata.rb
index 73a6833b7cc..de1d6dbf691 100644
--- a/spec/support/atlassian/jira_connect/schemata.rb
+++ b/spec/support/atlassian/jira_connect/schemata.rb
@@ -7,11 +7,11 @@ module Atlassian
{
'type' => 'object',
'additionalProperties' => false,
- 'required' => %w(
+ 'required' => %w[
schemaVersion pipelineId buildNumber updateSequenceNumber
displayName url state issueKeys testInfo references
lastUpdated
- ),
+ ],
'properties' => {
'schemaVersion' => schema_version_type,
'pipelineId' => { 'type' => 'string' },
@@ -24,7 +24,7 @@ module Atlassian
'issueKeys' => issue_keys_type,
'testInfo' => {
'type' => 'object',
- 'required' => %w(totalNumber numberPassed numberFailed numberSkipped),
+ 'required' => %w[totalNumber numberPassed numberFailed numberSkipped],
'properties' => {
'totalNumber' => { 'type' => 'integer' },
'numberFailed' => { 'type' => 'integer' },
@@ -36,11 +36,11 @@ module Atlassian
'type' => 'array',
'items' => {
'type' => 'object',
- 'required' => %w(commit ref),
+ 'required' => %w[commit ref],
'properties' => {
'commit' => {
'type' => 'object',
- 'required' => %w(id repositoryUri),
+ 'required' => %w[id repositoryUri],
'properties' => {
'id' => { 'type' => 'string' },
'repositoryUri' => { 'type' => 'string' }
@@ -48,7 +48,7 @@ module Atlassian
},
'ref' => {
'type' => 'object',
- 'required' => %w(name uri),
+ 'required' => %w[name uri],
'properties' => {
'name' => { 'type' => 'string' },
'uri' => { 'type' => 'string' }
@@ -65,16 +65,16 @@ module Atlassian
{
'type' => 'object',
'additionalProperties' => false,
- 'required' => %w(
+ 'required' => %w[
deploymentSequenceNumber updateSequenceNumber
associations displayName url description lastUpdated
state pipeline environment
- ),
+ ],
'properties' => {
'deploymentSequenceNumber' => { 'type' => 'integer' },
'updateSequenceNumber' => { 'type' => 'integer' },
'associations' => {
- 'type' => %w(array),
+ 'type' => %w[array],
'items' => association_type,
'minItems' => 1
},
@@ -95,9 +95,9 @@ module Atlassian
{
'type' => 'object',
'additionalProperties' => false,
- 'required' => %w(
+ 'required' => %w[
updateSequenceId id key issueKeys summary details
- ),
+ ],
'properties' => {
'id' => { 'type' => 'string' },
'key' => { 'type' => 'string' },
@@ -120,7 +120,7 @@ module Atlassian
'environment' => {
'type' => 'object',
'additionalProperties' => false,
- 'required' => %w(name),
+ 'required' => %w[name],
'properties' => {
'name' => { 'type' => 'string' },
'type' => {
@@ -144,7 +144,7 @@ module Atlassian
{
'type' => 'object',
'additionalProperties' => false,
- 'required' => %w(url status lastUpdated),
+ 'required' => %w[url status lastUpdated],
'properties' => {
'lastUpdated' => iso8601_type,
'url' => { 'type' => 'string' },
@@ -157,7 +157,7 @@ module Atlassian
{
'type' => 'object',
'additionalProperties' => false,
- 'required' => %w(enabled),
+ 'required' => %w[enabled],
'properties' => {
'enabled' => { 'type' => 'boolean' },
'defaultValue' => { 'type' => 'string' },
@@ -182,7 +182,7 @@ module Atlassian
{
'type' => 'object',
'additionalProperties' => false,
- 'required' => %w(id displayName type),
+ 'required' => %w[id displayName type],
'properties' => {
'id' => { 'type' => 'string', 'maxLength' => 255 },
'displayName' => { 'type' => 'string', 'maxLength' => 255 },
@@ -198,7 +198,7 @@ module Atlassian
{
'type' => 'object',
'additionalProperties' => false,
- 'required' => %w(id displayName url),
+ 'required' => %w[id displayName url],
'properties' => {
'id' => { 'type' => 'string', 'maxLength' => 255 },
'displayName' => { 'type' => 'string', 'maxLength' => 255 },
@@ -222,7 +222,7 @@ module Atlassian
{
'type' => 'object',
'additionalProperties' => false,
- 'required' => %w(associationType values),
+ 'required' => %w[associationType values],
'properties' => {
'associationType' => {
'type' => 'string',
@@ -276,7 +276,7 @@ module Atlassian
def provider_metadata
{
'type' => 'object',
- 'required' => %w(product),
+ 'required' => %w[product],
'properties' => { 'product' => { 'type' => 'string' } }
}
end
diff --git a/spec/support/capybara.rb b/spec/support/capybara.rb
index 78d7e57c208..c8fa430c02c 100644
--- a/spec/support/capybara.rb
+++ b/spec/support/capybara.rb
@@ -191,7 +191,7 @@ RSpec.configure do |config|
if example.metadata[:screenshot]
screenshot = example.metadata[:screenshot][:image] || example.metadata[:screenshot][:html]
screenshot&.delete_prefix!(ENV.fetch('CI_PROJECT_DIR', ''))
- example.metadata[:stdout] = %{[[ATTACHMENT|#{screenshot}]]}
+ example.metadata[:stdout] = %([[ATTACHMENT|#{screenshot}]])
end
end
diff --git a/spec/support/capybara_slow_finder.rb b/spec/support/capybara_slow_finder.rb
index 975ddd52c1f..697b288e8b5 100644
--- a/spec/support/capybara_slow_finder.rb
+++ b/spec/support/capybara_slow_finder.rb
@@ -13,13 +13,13 @@ module Capybara
# Inspired by https://github.com/ngauthier/capybara-slow_finder_errors
module SlowFinder
def synchronize(seconds = nil, errors: nil)
- start_time = Gitlab::Metrics::System.monotonic_time
+ start_time = ::Gitlab::Metrics::System.monotonic_time
super
rescue Capybara::ElementNotFound => e
seconds ||= Capybara.default_max_wait_time
- raise e unless seconds > 0 && Gitlab::Metrics::System.monotonic_time - start_time > seconds
+ raise e unless seconds > 0 && ::Gitlab::Metrics::System.monotonic_time - start_time > seconds
message = format(MESSAGE, timeout: seconds)
raise e, "#{$!}\n\n#{message}", e.backtrace
diff --git a/spec/support/database/auto_explain.rb b/spec/support/database/auto_explain.rb
index 799457034a1..11f8f1f899b 100644
--- a/spec/support/database/auto_explain.rb
+++ b/spec/support/database/auto_explain.rb
@@ -119,9 +119,10 @@ module AutoExplain
return false if ENV['CI_JOB_NAME_SLUG'] == 'db-migrate-non-superuser'
return false if connection.database_version.to_s[0..1].to_i < 14
return false if connection.select_one('SHOW is_superuser')['is_superuser'] != 'on'
+ return false if connection.select_one('SELECT pg_stat_file(\'log/pglog.csv\', true)')['pg_stat_file'].nil?
- # This condition matches the pipeline rules for if-merge-request-labels-record-queries
- return true if ENV['CI_MERGE_REQUEST_LABELS']&.include?('pipeline:record-queries')
+ # This condition matches the pipeline rules for if-merge-request
+ return true if %w[detached merged_result].include?(ENV['CI_MERGE_REQUEST_EVENT_TYPE'])
# This condition matches the pipeline rules for if-default-branch-refs
ENV['CI_COMMIT_REF_NAME'] == ENV['CI_DEFAULT_BRANCH'] && !ENV['CI_MERGE_REQUEST_IID']
diff --git a/spec/support/database/click_house/hooks.rb b/spec/support/database/click_house/hooks.rb
index b970d3daf84..77b33b7aaa3 100644
--- a/spec/support/database/click_house/hooks.rb
+++ b/spec/support/database/click_house/hooks.rb
@@ -2,6 +2,8 @@
# rubocop: disable Gitlab/NamespacedClass
class ClickHouseTestRunner
+ include ClickHouseTestHelpers
+
def truncate_tables
ClickHouse::Client.configuration.databases.each_key do |db|
# Select tables with at least one row
@@ -9,6 +11,8 @@ class ClickHouseTestRunner
"(SELECT '#{table}' AS table FROM #{table} LIMIT 1)"
end.join(' UNION ALL ')
+ next if query.empty?
+
tables_with_data = ClickHouse::Client.select(query, db).pluck('table')
tables_with_data.each do |table|
ClickHouse::Client.execute("TRUNCATE TABLE #{table}", db)
@@ -19,17 +23,13 @@ class ClickHouseTestRunner
def ensure_schema
return if @ensure_schema
- ClickHouse::Client.configuration.databases.each_key do |db|
- # drop all tables
- lookup_tables(db).each do |table|
- ClickHouse::Client.execute("DROP TABLE IF EXISTS #{table}", db)
- end
+ clear_db
- # run the schema SQL files
- Dir[Rails.root.join("db/click_house/#{db}/*.sql")].each do |file|
- ClickHouse::Client.execute(File.read(file), db)
- end
- end
+ # run the schema SQL files
+ migrations_paths = ClickHouse::MigrationSupport::Migrator.migrations_paths
+ schema_migration = ClickHouse::MigrationSupport::SchemaMigration
+ migration_context = ClickHouse::MigrationSupport::MigrationContext.new(migrations_paths, schema_migration)
+ migrate(nil, migration_context)
@ensure_schema = true
end
@@ -38,11 +38,7 @@ class ClickHouseTestRunner
def tables_for(db)
@tables ||= {}
- @tables[db] ||= lookup_tables(db)
- end
-
- def lookup_tables(db)
- ClickHouse::Client.select('SHOW TABLES', db).pluck('name')
+ @tables[db] ||= lookup_tables(db) - [ClickHouse::MigrationSupport::SchemaMigration.table_name]
end
end
# rubocop: enable Gitlab/NamespacedClass
@@ -52,8 +48,12 @@ RSpec.configure do |config|
config.around(:each, :click_house) do |example|
with_net_connect_allowed do
- click_house_test_runner.ensure_schema
- click_house_test_runner.truncate_tables
+ if example.example.metadata[:click_house] == :without_migrations
+ click_house_test_runner.clear_db
+ else
+ click_house_test_runner.ensure_schema
+ click_house_test_runner.truncate_tables
+ end
example.run
end
diff --git a/spec/support/database/partitioning_routing_analyzer.rb b/spec/support/database/partitioning_routing_analyzer.rb
new file mode 100644
index 00000000000..b1edd817386
--- /dev/null
+++ b/spec/support/database/partitioning_routing_analyzer.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+RSpec.configure do |config|
+ config.around(:each, :suppress_partitioning_routing_analyzer) do |example|
+ Gitlab::Database::QueryAnalyzers::Ci::PartitioningRoutingAnalyzer.with_suppressed(&example)
+ end
+end
diff --git a/spec/support/db_cleaner.rb b/spec/support/db_cleaner.rb
index a1579ad1685..0a1d68a744c 100644
--- a/spec/support/db_cleaner.rb
+++ b/spec/support/db_cleaner.rb
@@ -20,7 +20,7 @@ module DbCleaner
def setup_database_cleaner
all_connection_classes.each do |connection_class|
- DatabaseCleaner[:active_record, { connection: connection_class }]
+ DatabaseCleaner[:active_record, db: connection_class]
end
end
@@ -57,7 +57,7 @@ module DbCleaner
end
def recreate_all_databases!
- start = Gitlab::Metrics::System.monotonic_time
+ start = ::Gitlab::Metrics::System.monotonic_time
puts "Recreating the database"
@@ -81,7 +81,7 @@ module DbCleaner
Gitlab::Database::Partitioning.sync_partitions_ignore_db_error
stub_feature_flags(disallow_database_ddl_feature_flags: disable_ddl_was)
- puts "Databases re-creation done in #{Gitlab::Metrics::System.monotonic_time - start}"
+ puts "Databases re-creation done in #{::Gitlab::Metrics::System.monotonic_time - start}"
end
def recreate_databases_and_seed_if_needed
diff --git a/spec/support/finder_collection_allowlist.yml b/spec/support/finder_collection_allowlist.yml
index 0af4de11d51..e60cc4278af 100644
--- a/spec/support/finder_collection_allowlist.yml
+++ b/spec/support/finder_collection_allowlist.yml
@@ -70,4 +70,3 @@
- UploaderFinder
- UserGroupNotificationSettingsFinder
- UserGroupsCounter
-- DataTransfer::MockedTransferFinder # Can be removed when https://gitlab.com/gitlab-org/gitlab/-/issues/397693 is closed
diff --git a/spec/support/helpers/api_internal_base_helpers.rb b/spec/support/helpers/api_internal_base_helpers.rb
index 0c334e164a6..d3ae1a5c3b2 100644
--- a/spec/support/helpers/api_internal_base_helpers.rb
+++ b/spec/support/helpers/api_internal_base_helpers.rb
@@ -41,18 +41,19 @@ module APIInternalBaseHelpers
)
end
- def push(key, container, protocol = 'ssh', env: nil, changes: nil)
+ def push(key, container, protocol = 'ssh', env: nil, changes: nil, relative_path: nil)
push_with_path(
key,
full_path: full_path_for(container),
gl_repository: gl_repository_for(container),
protocol: protocol,
env: env,
- changes: changes
+ changes: changes,
+ relative_path: relative_path
)
end
- def push_with_path(key, full_path:, gl_repository: nil, protocol: 'ssh', env: nil, changes: nil)
+ def push_with_path(key, full_path:, gl_repository: nil, protocol: 'ssh', env: nil, changes: nil, relative_path: nil)
changes ||= 'd14d6c0abdd253381df51a723d58691b2ee1ab08 570e7b2abdd848b95f2f578043fc23bd6f6fd24d refs/heads/master'
params = {
@@ -61,7 +62,8 @@ module APIInternalBaseHelpers
project: full_path,
action: 'git-receive-pack',
protocol: protocol,
- env: env
+ env: env,
+ relative_path: relative_path
}
params[:gl_repository] = gl_repository if gl_repository
diff --git a/spec/support/helpers/click_house_test_helpers.rb b/spec/support/helpers/click_house_test_helpers.rb
new file mode 100644
index 00000000000..24f81a3ec01
--- /dev/null
+++ b/spec/support/helpers/click_house_test_helpers.rb
@@ -0,0 +1,85 @@
+# frozen_string_literal: true
+
+module ClickHouseTestHelpers
+ def migrate(target_version, migration_context)
+ quietly { migration_context.up(target_version) }
+ end
+
+ def rollback(target_version, migration_context)
+ quietly { migration_context.down(target_version) }
+ end
+
+ def table_names(database = :main, configuration = ClickHouse::Client.configuration)
+ ClickHouse::Client.select('SHOW TABLES', database, configuration).pluck('name')
+ end
+
+ def active_schema_migrations_count(database = :main, configuration = ClickHouse::Client.configuration)
+ query = <<~SQL
+ SELECT COUNT(*) AS count FROM schema_migrations FINAL WHERE active = 1
+ SQL
+
+ ClickHouse::Client.select(query, database, configuration).first['count']
+ end
+
+ def describe_table(table_name, database = :main, configuration = ClickHouse::Client.configuration)
+ ClickHouse::Client
+ .select("DESCRIBE TABLE #{table_name} FORMAT JSON", database, configuration)
+ .map(&:symbolize_keys)
+ .index_by { |h| h[:name].to_sym }
+ end
+
+ def schema_migrations(database = :main, configuration = ClickHouse::Client.configuration)
+ ClickHouse::Client
+ .select('SELECT * FROM schema_migrations FINAL ORDER BY version ASC', database, configuration)
+ .map(&:symbolize_keys)
+ end
+
+ def clear_db(configuration = ClickHouse::Client.configuration)
+ configuration.databases.each_key do |db|
+ # drop all tables
+ lookup_tables(db, configuration).each do |table|
+ ClickHouse::Client.execute("DROP TABLE IF EXISTS #{table}", db, configuration)
+ end
+
+ ClickHouse::MigrationSupport::SchemaMigration.create_table(db, configuration)
+ end
+ end
+
+ def register_database(config, database_identifier, db_config)
+ config.register_database(
+ database_identifier,
+ database: db_config[:database],
+ url: db_config[:url],
+ username: db_config[:username],
+ password: db_config[:password],
+ variables: db_config[:variables] || {}
+ )
+ end
+
+ private
+
+ def lookup_tables(db, configuration = ClickHouse::Client.configuration)
+ ClickHouse::Client.select('SHOW TABLES', db, configuration).pluck('name')
+ end
+
+ def quietly(&_block)
+ was_verbose = ClickHouse::Migration.verbose
+ ClickHouse::Migration.verbose = false
+
+ yield
+ ensure
+ ClickHouse::Migration.verbose = was_verbose
+ end
+
+ def clear_consts(fixtures_path)
+ $LOADED_FEATURES.select { |file| file.include? fixtures_path }.each do |file|
+ const = File.basename(file)
+ .scan(ClickHouse::Migration::MIGRATION_FILENAME_REGEXP)[0][1]
+ .camelcase
+ .safe_constantize
+
+ Object.send(:remove_const, const.to_s) if const
+ $LOADED_FEATURES.delete(file)
+ end
+ end
+end
diff --git a/spec/support/helpers/crypto_helpers.rb b/spec/support/helpers/crypto_helpers.rb
new file mode 100644
index 00000000000..0b2d5f6386a
--- /dev/null
+++ b/spec/support/helpers/crypto_helpers.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+module CryptoHelpers
+ def sha256(value)
+ Gitlab::CryptoHelper.sha256(value)
+ end
+end
diff --git a/spec/support/helpers/cycle_analytics_helpers.rb b/spec/support/helpers/cycle_analytics_helpers.rb
index 5f60f8a6bfa..890fefcc7de 100644
--- a/spec/support/helpers/cycle_analytics_helpers.rb
+++ b/spec/support/helpers/cycle_analytics_helpers.rb
@@ -1,6 +1,10 @@
# frozen_string_literal: true
+require_relative './listbox_helpers'
+
module CycleAnalyticsHelpers
+ include ::ListboxHelpers
+
def toggle_value_stream_dropdown
page.find('[data-testid="dropdown-value-streams"]').click
end
@@ -16,8 +20,8 @@ module CycleAnalyticsHelpers
within last_stage do
find('[name*="custom-stage-name-"]').fill_in with: "Cool custom stage - name #{index}"
- select_dropdown_option_by_value "custom-stage-start-event-", :merge_request_created
- select_dropdown_option_by_value "custom-stage-end-event-", :merge_request_merged
+ select_dropdown_option_by_value "custom-stage-start-event-", 'Merge request created'
+ select_dropdown_option_by_value "custom-stage-end-event-", 'Merge request merged'
end
end
@@ -34,8 +38,8 @@ module CycleAnalyticsHelpers
within last_stage do
find('[name*="custom-stage-name-"]').fill_in with: "Cool custom label stage - name #{index}"
- select_dropdown_option_by_value "custom-stage-start-event-", :issue_label_added
- select_dropdown_option_by_value "custom-stage-end-event-", :issue_label_removed
+ select_dropdown_option_by_value "custom-stage-start-event-", 'Issue label was added'
+ select_dropdown_option_by_value "custom-stage-end-event-", 'Issue label was removed'
select_event_label("[data-testid*='custom-stage-start-event-label-']")
select_event_label("[data-testid*='custom-stage-end-event-label-']")
@@ -102,19 +106,14 @@ module CycleAnalyticsHelpers
select_value_stream(custom_value_stream_name)
end
- def toggle_dropdown(field)
- page.within("[data-testid*='#{field}']") do
- find('.dropdown-toggle').click
+ def select_dropdown_option_by_value(name, value)
+ page.within("[data-testid*='#{name}']") do
+ toggle_listbox
wait_for_requests
-
- expect(find('.dropdown-menu')).to have_selector('.dropdown-item')
end
- end
- def select_dropdown_option_by_value(name, value, elem = '.dropdown-item')
- toggle_dropdown name
- page.find("[data-testid*='#{name}'] .dropdown-menu").find("#{elem}[value='#{value}']").click
+ select_listbox_item(value)
end
def create_commit_referencing_issue(issue, branch_name: generate(:branch))
diff --git a/spec/support/helpers/cycle_analytics_helpers/test_generation.rb b/spec/support/helpers/cycle_analytics_helpers/test_generation.rb
deleted file mode 100644
index 1c7c45c06a1..00000000000
--- a/spec/support/helpers/cycle_analytics_helpers/test_generation.rb
+++ /dev/null
@@ -1,166 +0,0 @@
-# frozen_string_literal: true
-
-# rubocop:disable Layout/LineLength
-# rubocop:disable Metrics/CyclomaticComplexity
-# rubocop:disable Metrics/PerceivedComplexity
-# rubocop:disable Metrics/AbcSize
-
-# Note: The ABC size is large here because we have a method generating test cases with
-# multiple nested contexts. This shouldn't count as a violation.
-module CycleAnalyticsHelpers
- module TestGeneration
- # Generate the most common set of specs that all value stream analytics phases need to have.
- #
- # Arguments:
- #
- # phase: Which phase are we testing? Will call `CycleAnalytics.new.send(phase)` for the final assertion
- # data_fn: A function that returns a hash, constituting initial data for the test case
- # start_time_conditions: An array of `conditions`. Each condition is an tuple of `condition_name` and `condition_fn`. `condition_fn` is called with
- # `context` (no lexical scope, so need to do `context.create` for factories, for example) and `data` (from the `data_fn`).
- # Each `condition_fn` is expected to implement a case which consitutes the start of the given value stream analytics phase.
- # end_time_conditions: An array of `conditions`. Each condition is an tuple of `condition_name` and `condition_fn`. `condition_fn` is called with
- # `context` (no lexical scope, so need to do `context.create` for factories, for example) and `data` (from the `data_fn`).
- # Each `condition_fn` is expected to implement a case which consitutes the end of the given value stream analytics phase.
- # before_end_fn: This function is run before calling the end time conditions. Used for setup that needs to be run between the start and end conditions.
- # post_fn: Code that needs to be run after running the end time conditions.
-
- def generate_cycle_analytics_spec(phase:, data_fn:, start_time_conditions:, end_time_conditions:, before_end_fn: nil, post_fn: nil)
- combinations_of_start_time_conditions = (1..start_time_conditions.size).flat_map { |size| start_time_conditions.combination(size).to_a }
- combinations_of_end_time_conditions = (1..end_time_conditions.size).flat_map { |size| end_time_conditions.combination(size).to_a }
-
- scenarios = combinations_of_start_time_conditions.product(combinations_of_end_time_conditions)
- scenarios.each do |start_time_conditions, end_time_conditions|
- let_it_be(:other_project) { create(:project, :repository) }
-
- before do
- other_project.add_developer(user)
- end
-
- context "start condition: #{start_time_conditions.map(&:first).to_sentence}" do
- context "end condition: #{end_time_conditions.map(&:first).to_sentence}" do
- it "finds the median of available durations between the two conditions", :sidekiq_might_not_need_inline do
- time_differences = Array.new(5) do |index|
- data = data_fn[self]
- start_time = (index * 10).days.from_now
- end_time = start_time + rand(1..5).days
-
- start_time_conditions.each_value do |condition_fn|
- travel_to(start_time) { condition_fn[self, data] }
- end
-
- # Run `before_end_fn` at the midpoint between `start_time` and `end_time`
- travel_to(start_time + ((end_time - start_time) / 2)) { before_end_fn[self, data] } if before_end_fn
-
- end_time_conditions.each_value do |condition_fn|
- travel_to(end_time) { condition_fn[self, data] }
- end
-
- travel_to(end_time + 1.day) { post_fn[self, data] } if post_fn
-
- end_time - start_time
- end
-
- median_time_difference = time_differences.sort[2]
- expect(subject[phase].project_median).to be_within(5).of(median_time_difference)
- end
-
- context "when the data belongs to another project" do
- it "returns nil" do
- # Use a stub to "trick" the data/condition functions
- # into using another project. This saves us from having to
- # define separate data/condition functions for this particular
- # test case.
- allow(self).to receive(:project) { other_project }
-
- data = data_fn[self]
- start_time = Time.now
- end_time = rand(1..10).days.from_now
-
- start_time_conditions.each_value do |condition_fn|
- travel_to(start_time) { condition_fn[self, data] }
- end
-
- end_time_conditions.each_value do |condition_fn|
- travel_to(end_time) { condition_fn[self, data] }
- end
-
- travel_to(end_time + 1.day) { post_fn[self, data] } if post_fn
-
- # Turn off the stub before checking assertions
- allow(self).to receive(:project).and_call_original
-
- expect(subject[phase].project_median).to be_nil
- end
- end
-
- context "when the end condition happens before the start condition" do
- it 'returns nil' do
- data = data_fn[self]
- start_time = Time.now
- end_time = start_time + rand(1..5).days
-
- # Run `before_end_fn` at the midpoint between `start_time` and `end_time`
- travel_to(start_time + ((end_time - start_time) / 2)) { before_end_fn[self, data] } if before_end_fn
-
- end_time_conditions.each_value do |condition_fn|
- travel_to(start_time) { condition_fn[self, data] }
- end
-
- start_time_conditions.each_value do |condition_fn|
- travel_to(end_time) { condition_fn[self, data] }
- end
-
- travel_to(end_time + 1.day) { post_fn[self, data] } if post_fn
-
- expect(subject[phase].project_median).to be_nil
- end
- end
- end
-
- context "end condition NOT PRESENT: #{end_time_conditions.map(&:first).to_sentence}" do
- it "returns nil" do
- data = data_fn[self]
- start_time = Time.now
-
- start_time_conditions.each_value do |condition_fn|
- travel_to(start_time) { condition_fn[self, data] }
- end
-
- post_fn[self, data] if post_fn
-
- expect(subject[phase].project_median).to be_nil
- end
- end
- end
-
- context "start condition NOT PRESENT: #{start_time_conditions.map(&:first).to_sentence}" do
- context "end condition: #{end_time_conditions.map(&:first).to_sentence}" do
- it "returns nil" do
- data = data_fn[self]
- end_time = rand(1..10).days.from_now
-
- end_time_conditions.each_with_index do |(_condition_name, condition_fn), index|
- travel_to(end_time + index.days) { condition_fn[self, data] }
- end
-
- travel_to(end_time + 1.day) { post_fn[self, data] } if post_fn
-
- expect(subject[phase].project_median).to be_nil
- end
- end
- end
- end
-
- context "when none of the start / end conditions are matched" do
- it "returns nil" do
- expect(subject[phase].project_median).to be_nil
- end
- end
- end
- end
-end
-
-# rubocop:enable Layout/LineLength
-# rubocop:enable Metrics/CyclomaticComplexity
-# rubocop:enable Metrics/PerceivedComplexity
-# rubocop:enable Metrics/AbcSize
diff --git a/spec/support/helpers/database/duplicate_indexes.yml b/spec/support/helpers/database/duplicate_indexes.yml
index 02efdabd70b..1ebc45a9d81 100644
--- a/spec/support/helpers/database/duplicate_indexes.yml
+++ b/spec/support/helpers/database/duplicate_indexes.yml
@@ -2,264 +2,245 @@
# It maps table_name to {index1: array_of_duplicate_indexes, index2: array_of_duplicate_indexes, ... }
abuse_reports:
idx_abuse_reports_user_id_status_and_category:
- - index_abuse_reports_on_user_id
+ - index_abuse_reports_on_user_id
alert_management_http_integrations:
index_http_integrations_on_project_and_endpoint:
- - index_alert_management_http_integrations_on_project_id
-analytics_cycle_analytics_group_stages:
- index_group_stages_on_group_id_group_value_stream_id_and_name:
- - index_analytics_ca_group_stages_on_group_id
+ - index_alert_management_http_integrations_on_project_id
approval_project_rules_users:
index_approval_project_rules_users_1:
- - index_approval_project_rules_users_on_approval_project_rule_id
+ - index_approval_project_rules_users_on_approval_project_rule_id
approvals:
index_approvals_on_merge_request_id_and_created_at:
- - index_approvals_on_merge_request_id
+ - index_approvals_on_merge_request_id
board_group_recent_visits:
index_board_group_recent_visits_on_user_group_and_board:
- - index_board_group_recent_visits_on_user_id
+ - index_board_group_recent_visits_on_user_id
board_project_recent_visits:
index_board_project_recent_visits_on_user_project_and_board:
- - index_board_project_recent_visits_on_user_id
+ - index_board_project_recent_visits_on_user_id
board_user_preferences:
index_board_user_preferences_on_user_id_and_board_id:
- - index_board_user_preferences_on_user_id
+ - index_board_user_preferences_on_user_id
boards_epic_board_recent_visits:
index_epic_board_recent_visits_on_user_group_and_board:
- - index_boards_epic_board_recent_visits_on_user_id
+ - index_boards_epic_board_recent_visits_on_user_id
boards_epic_user_preferences:
index_boards_epic_user_preferences_on_board_user_epic_unique:
- - index_boards_epic_user_preferences_on_board_id
+ - index_boards_epic_user_preferences_on_board_id
bulk_import_batch_trackers:
i_bulk_import_trackers_id_batch_number:
- - index_bulk_import_batch_trackers_on_tracker_id
+ - index_bulk_import_batch_trackers_on_tracker_id
bulk_import_export_batches:
i_bulk_import_export_batches_id_batch_number:
- - index_bulk_import_export_batches_on_export_id
+ - index_bulk_import_export_batches_on_export_id
ci_job_artifacts:
index_ci_job_artifacts_on_id_project_id_and_created_at:
- - index_ci_job_artifacts_on_project_id
+ - index_ci_job_artifacts_on_project_id
index_ci_job_artifacts_on_id_project_id_and_file_type:
- - index_ci_job_artifacts_on_project_id
+ - index_ci_job_artifacts_on_project_id
index_ci_job_artifacts_on_project_id_and_id:
- - index_ci_job_artifacts_on_project_id
+ - index_ci_job_artifacts_on_project_id
ci_pipeline_artifacts:
index_ci_pipeline_artifacts_on_pipeline_id_and_file_type:
- - index_ci_pipeline_artifacts_on_pipeline_id
+ - index_ci_pipeline_artifacts_on_pipeline_id
ci_stages:
index_ci_stages_on_pipeline_id_and_name:
- - index_ci_stages_on_pipeline_id
+ - index_ci_stages_on_pipeline_id
index_ci_stages_on_pipeline_id_and_position:
- - index_ci_stages_on_pipeline_id
+ - index_ci_stages_on_pipeline_id
index_ci_stages_on_pipeline_id_convert_to_bigint_and_name:
- - index_ci_stages_on_pipeline_id_convert_to_bigint
+ - index_ci_stages_on_pipeline_id_convert_to_bigint
index_ci_stages_on_pipeline_id_convert_to_bigint_and_position:
- - index_ci_stages_on_pipeline_id_convert_to_bigint
+ - index_ci_stages_on_pipeline_id_convert_to_bigint
dast_site_tokens:
index_dast_site_token_on_project_id_and_url:
- - index_dast_site_tokens_on_project_id
+ - index_dast_site_tokens_on_project_id
design_management_designs:
index_design_management_designs_on_iid_and_project_id:
- - index_design_management_designs_on_project_id
+ - index_design_management_designs_on_project_id
design_management_designs_versions:
design_management_designs_versions_uniqueness:
- - index_design_management_designs_versions_on_design_id
+ - index_design_management_designs_versions_on_design_id
error_tracking_errors:
index_et_errors_on_project_id_and_status_and_id:
- - index_error_tracking_errors_on_project_id
+ - index_error_tracking_errors_on_project_id
index_et_errors_on_project_id_and_status_events_count_id_desc:
- - index_error_tracking_errors_on_project_id
+ - index_error_tracking_errors_on_project_id
index_et_errors_on_project_id_and_status_first_seen_at_id_desc:
- - index_error_tracking_errors_on_project_id
+ - index_error_tracking_errors_on_project_id
index_et_errors_on_project_id_and_status_last_seen_at_id_desc:
- - index_error_tracking_errors_on_project_id
+ - index_error_tracking_errors_on_project_id
geo_node_namespace_links:
index_geo_node_namespace_links_on_geo_node_id_and_namespace_id:
- - index_geo_node_namespace_links_on_geo_node_id
+ - index_geo_node_namespace_links_on_geo_node_id
in_product_marketing_emails:
- index_in_product_marketing_emails_on_user_campaign:
- - index_in_product_marketing_emails_on_user_id
index_in_product_marketing_emails_on_user_track_series:
- - index_in_product_marketing_emails_on_user_id
+ - index_in_product_marketing_emails_on_user_id
incident_management_oncall_participants:
index_inc_mgmnt_oncall_participants_on_user_id_and_rotation_id:
- - index_inc_mgmnt_oncall_participants_on_oncall_user_id
+ - index_inc_mgmnt_oncall_participants_on_oncall_user_id
incident_management_oncall_schedules:
index_im_oncall_schedules_on_project_id_and_iid:
- - index_incident_management_oncall_schedules_on_project_id
+ - index_incident_management_oncall_schedules_on_project_id
instance_audit_events_streaming_headers:
idx_instance_external_audit_event_destination_id_key_uniq:
- - idx_headers_instance_external_audit_event_destination_id
+ - idx_headers_instance_external_audit_event_destination_id
issue_links:
index_issue_links_on_source_id_and_target_id:
- - index_issue_links_on_source_id
+ - index_issue_links_on_source_id
issues:
index_issues_on_author_id_and_id_and_created_at:
- - index_issues_on_author_id
+ - index_issues_on_author_id
jira_connect_subscriptions:
idx_jira_connect_subscriptions_on_installation_id_namespace_id:
- - idx_jira_connect_subscriptions_on_installation_id
+ - idx_jira_connect_subscriptions_on_installation_id
list_user_preferences:
index_list_user_preferences_on_user_id_and_list_id:
- - index_list_user_preferences_on_user_id
+ - index_list_user_preferences_on_user_id
member_tasks:
index_member_tasks_on_member_id_and_project_id:
- - index_member_tasks_on_member_id
+ - index_member_tasks_on_member_id
members:
index_members_on_member_namespace_id_compound:
- - index_members_on_member_namespace_id
-merge_request_assignees:
- index_merge_request_assignees_on_merge_request_id_and_user_id:
- - index_merge_request_assignees_on_merge_request_id
-merge_request_metrics:
- index_mr_metrics_on_target_project_id_merged_at_nulls_last:
- - index_merge_request_metrics_on_target_project_id
+ - index_members_on_member_namespace_id
merge_requests:
index_merge_requests_on_author_id_and_created_at:
- - index_merge_requests_on_author_id
+ - index_merge_requests_on_author_id
index_merge_requests_on_author_id_and_id:
- - index_merge_requests_on_author_id
+ - index_merge_requests_on_author_id
index_merge_requests_on_author_id_and_target_project_id:
- - index_merge_requests_on_author_id
+ - index_merge_requests_on_author_id
ml_candidate_params:
index_ml_candidate_params_on_candidate_id_on_name:
- - index_ml_candidate_params_on_candidate_id
+ - index_ml_candidate_params_on_candidate_id
ml_candidates:
index_ml_candidates_on_project_id_on_internal_id:
- - index_ml_candidates_on_project_id
+ - index_ml_candidates_on_project_id
ml_model_versions:
index_ml_model_versions_on_project_id_and_model_id_and_version:
- - index_ml_model_versions_on_project_id
+ - index_ml_model_versions_on_project_id
ml_models:
index_ml_models_on_project_id_and_name:
- - index_ml_models_on_project_id
+ - index_ml_models_on_project_id
p_ci_runner_machine_builds:
index_p_ci_runner_machine_builds_on_runner_machine_id:
- - index_ci_runner_machine_builds_on_runner_machine_id
+ - index_ci_runner_machine_builds_on_runner_machine_id
packages_debian_group_distributions:
uniq_pkgs_debian_group_distributions_group_id_and_codename:
- - index_packages_debian_group_distributions_on_group_id
+ - index_packages_debian_group_distributions_on_group_id
uniq_pkgs_debian_group_distributions_group_id_and_suite:
- - index_packages_debian_group_distributions_on_group_id
+ - index_packages_debian_group_distributions_on_group_id
packages_debian_project_distributions:
uniq_pkgs_debian_project_distributions_project_id_and_codename:
- - index_packages_debian_project_distributions_on_project_id
+ - index_packages_debian_project_distributions_on_project_id
uniq_pkgs_debian_project_distributions_project_id_and_suite:
- - index_packages_debian_project_distributions_on_project_id
+ - index_packages_debian_project_distributions_on_project_id
packages_tags:
index_packages_tags_on_package_id_and_updated_at:
- - index_packages_tags_on_package_id
+ - index_packages_tags_on_package_id
pages_domains:
index_pages_domains_on_project_id_and_enabled_until:
- - index_pages_domains_on_project_id
+ - index_pages_domains_on_project_id
index_pages_domains_on_verified_at_and_enabled_until:
- - index_pages_domains_on_verified_at
+ - index_pages_domains_on_verified_at
personal_access_tokens:
index_pat_on_user_id_and_expires_at:
- - index_personal_access_tokens_on_user_id
+ - index_personal_access_tokens_on_user_id
pm_affected_packages:
i_affected_packages_unique_for_upsert:
- - index_pm_affected_packages_on_pm_advisory_id
+ - index_pm_affected_packages_on_pm_advisory_id
pm_package_version_licenses:
i_pm_package_version_licenses_join_ids:
- - index_pm_package_version_licenses_on_pm_package_version_id
+ - index_pm_package_version_licenses_on_pm_package_version_id
pm_package_versions:
i_pm_package_versions_on_package_id_and_version:
- - index_pm_package_versions_on_pm_package_id
+ - index_pm_package_versions_on_pm_package_id
project_compliance_standards_adherence:
u_project_compliance_standards_adherence_for_reporting:
- - index_project_compliance_standards_adherence_on_project_id
+ - index_project_compliance_standards_adherence_on_project_id
project_relation_exports:
index_project_export_job_relation:
- - index_project_relation_exports_on_project_export_job_id
+ - index_project_relation_exports_on_project_export_job_id
project_repositories:
index_project_repositories_on_shard_id_and_project_id:
- - index_project_repositories_on_shard_id
+ - index_project_repositories_on_shard_id
project_topics:
index_project_topics_on_project_id_and_topic_id:
- - index_project_topics_on_project_id
-projects:
- index_projects_api_path_id_desc:
- - index_on_projects_path
- index_projects_on_path_and_id:
- - index_on_projects_path
+ - index_project_topics_on_project_id
protected_environments:
index_protected_environments_on_project_id_and_name:
- - index_protected_environments_on_project_id
+ - index_protected_environments_on_project_id
protected_tags:
index_protected_tags_on_project_id_and_name:
- - index_protected_tags_on_project_id
+ - index_protected_tags_on_project_id
related_epic_links:
index_related_epic_links_on_source_id_and_target_id:
- - index_related_epic_links_on_source_id
+ - index_related_epic_links_on_source_id
requirements_management_test_reports:
idx_test_reports_on_issue_id_created_at_and_id:
- - index_requirements_management_test_reports_on_issue_id
+ - index_requirements_management_test_reports_on_issue_id
sbom_component_versions:
index_sbom_component_versions_on_component_id_and_version:
- - index_sbom_component_versions_on_component_id
+ - index_sbom_component_versions_on_component_id
sbom_occurrences:
index_sbom_occurrences_for_input_file_path_search:
- - index_sbom_occurrences_on_project_id_component_id
- - index_sbom_occurrences_on_project_id
+ - index_sbom_occurrences_on_project_id_component_id
+ - index_sbom_occurrences_on_project_id
idx_sbom_occurrences_on_project_id_and_source_id:
- - index_sbom_occurrences_on_project_id
+ - index_sbom_occurrences_on_project_id
index_sbom_occurrences_on_project_id_and_id:
- - index_sbom_occurrences_on_project_id
+ - index_sbom_occurrences_on_project_id
index_sbom_occurrences_on_project_id_component_id:
- - index_sbom_occurrences_on_project_id
+ - index_sbom_occurrences_on_project_id
index_sbom_occurrences_on_project_id_and_component_id_and_id:
- - index_sbom_occurrences_on_project_id_component_id
- - index_sbom_occurrences_on_project_id
+ - index_sbom_occurrences_on_project_id_component_id
+ - index_sbom_occurrences_on_project_id
index_sbom_occurrences_on_project_id_and_package_manager:
- - index_sbom_occurrences_on_project_id
-scan_result_policies:
- index_scan_result_policies_on_position_in_configuration:
- - index_scan_result_policies_on_policy_configuration_id
+ - index_sbom_occurrences_on_project_id
search_namespace_index_assignments:
index_search_namespace_index_assignments_uniqueness_index_type:
- - index_search_namespace_index_assignments_on_namespace_id
+ - index_search_namespace_index_assignments_on_namespace_id
index_search_namespace_index_assignments_uniqueness_on_index_id:
- - index_search_namespace_index_assignments_on_namespace_id
+ - index_search_namespace_index_assignments_on_namespace_id
sprints:
sequence_is_unique_per_iterations_cadence_id:
- - index_sprints_iterations_cadence_id
+ - index_sprints_iterations_cadence_id
taggings:
taggings_idx:
- - index_taggings_on_tag_id
+ - index_taggings_on_tag_id
term_agreements:
term_agreements_unique_index:
- - index_term_agreements_on_user_id
+ - index_term_agreements_on_user_id
todos:
index_todos_on_author_id_and_created_at:
- - index_todos_on_author_id
+ - index_todos_on_author_id
user_callouts:
index_user_callouts_on_user_id_and_feature_name:
- - index_user_callouts_on_user_id
+ - index_user_callouts_on_user_id
users:
index_users_on_state_and_user_type:
- - index_users_on_state
+ - index_users_on_state
vulnerabilities:
index_vulnerabilities_project_id_state_severity_default_branch:
- - index_vulnerabilities_on_project_id_and_state_and_severity
+ - index_vulnerabilities_on_project_id_and_state_and_severity
vulnerability_external_issue_links:
idx_vulnerability_ext_issue_links_on_vulne_id_and_ext_issue:
- - index_vulnerability_external_issue_links_on_vulnerability_id
+ - index_vulnerability_external_issue_links_on_vulnerability_id
vulnerability_finding_links:
finding_link_name_url_idx:
- - finding_links_on_vulnerability_occurrence_id
+ - finding_links_on_vulnerability_occurrence_id
vulnerability_finding_signatures:
idx_vuln_signatures_uniqueness_signature_sha:
- - index_vulnerability_finding_signatures_on_finding_id
+ - index_vulnerability_finding_signatures_on_finding_id
vulnerability_flags:
index_vulnerability_flags_on_unique_columns:
- - index_vulnerability_flags_on_vulnerability_occurrence_id
+ - index_vulnerability_flags_on_vulnerability_occurrence_id
web_hook_logs:
index_web_hook_logs_on_web_hook_id_and_created_at:
- - index_web_hook_logs_part_on_web_hook_id
+ - index_web_hook_logs_part_on_web_hook_id
web_hooks:
index_web_hooks_on_project_id_recent_failures:
- - index_web_hooks_on_project_id
+ - index_web_hooks_on_project_id
work_item_hierarchy_restrictions:
index_work_item_hierarchy_restrictions_on_parent_and_child:
- - index_work_item_hierarchy_restrictions_on_parent_type_id
+ - index_work_item_hierarchy_restrictions_on_parent_type_id
diff --git a/spec/support/helpers/email_helpers.rb b/spec/support/helpers/email_helpers.rb
index 57386233775..9dffe035b2a 100644
--- a/spec/support/helpers/email_helpers.rb
+++ b/spec/support/helpers/email_helpers.rb
@@ -94,7 +94,8 @@ module EmailHelpers
port: credential.smtp_port,
user_name: credential.smtp_username,
password: credential.smtp_password,
- domain: service_desk_setting.custom_email.split('@').last
+ domain: service_desk_setting.custom_email.split('@').last,
+ authentication: credential.smtp_authentication
)
end
end
diff --git a/spec/support/helpers/features/dom_helpers.rb b/spec/support/helpers/features/dom_helpers.rb
index cbbb80dde36..619f16f5e6d 100644
--- a/spec/support/helpers/features/dom_helpers.rb
+++ b/spec/support/helpers/features/dom_helpers.rb
@@ -2,6 +2,10 @@
module Features
module DomHelpers
+ def has_testid?(testid, **kwargs)
+ page.has_selector?("[data-testid='#{testid}']", **kwargs)
+ end
+
def find_by_testid(testid, **kwargs)
page.find("[data-testid='#{testid}']", **kwargs)
end
diff --git a/spec/support/helpers/features/releases_helpers.rb b/spec/support/helpers/features/releases_helpers.rb
index d5846aad15d..c3fbd128a28 100644
--- a/spec/support/helpers/features/releases_helpers.rb
+++ b/spec/support/helpers/features/releases_helpers.rb
@@ -44,20 +44,22 @@ module Features
end
def fill_release_title(release_title)
- fill_in('Release title', with: release_title)
+ fill_in('release-title', with: release_title)
end
- def select_milestone(milestone_title)
- page.within '[data-testid="milestones-field"]' do
- find('button').click
+ def select_milestones(*milestone_titles)
+ within_testid 'milestones-field' do
+ find_by_testid('base-dropdown-toggle').click
wait_for_all_requests
- find('input[aria-label="Search Milestones"]').set(milestone_title)
+ milestone_titles.each do |milestone_title|
+ find('input[type="search"]').set(milestone_title)
- wait_for_all_requests
+ wait_for_all_requests
- find('button', text: milestone_title, match: :first).click
+ find('[role="option"]', text: milestone_title).click
+ end
end
end
diff --git a/spec/support/helpers/gitaly_setup.rb b/spec/support/helpers/gitaly_setup.rb
index 06390406efc..f263b3b44ce 100644
--- a/spec/support/helpers/gitaly_setup.rb
+++ b/spec/support/helpers/gitaly_setup.rb
@@ -72,7 +72,9 @@ module GitalySetup
end
def repos_path(storage = REPOS_STORAGE)
- Gitlab.config.repositories.storages[REPOS_STORAGE].legacy_disk_path
+ Gitlab::GitalyClient::StorageSettings.allow_disk_access do
+ Gitlab.config.repositories.storages[REPOS_STORAGE].legacy_disk_path
+ end
end
def service_cmd(service, toml = nil)
diff --git a/spec/support/helpers/gpg_helpers.rb b/spec/support/helpers/gpg_helpers.rb
index cc8ee6c98e6..1f93b1bb698 100644
--- a/spec/support/helpers/gpg_helpers.rb
+++ b/spec/support/helpers/gpg_helpers.rb
@@ -700,7 +700,7 @@ module GpgHelpers
end
def subkey_fingerprints
- %w(65A33805A5DDA7454190EE536F0E46B850B18E99 3AD06974F78DD1603D5E4617D0955D22F2C324E2)
+ %w[65A33805A5DDA7454190EE536F0E46B850B18E99 3AD06974F78DD1603D5E4617D0955D22F2C324E2]
end
def names
diff --git a/spec/support/helpers/graphql_helpers.rb b/spec/support/helpers/graphql_helpers.rb
index 5eba982e3da..085340d6cb9 100644
--- a/spec/support/helpers/graphql_helpers.rb
+++ b/spec/support/helpers/graphql_helpers.rb
@@ -2,6 +2,7 @@
module GraphqlHelpers
def self.included(base)
+ base.include(::ApiHelpers)
base.include(::Gitlab::Graphql::Laziness)
end
diff --git a/spec/support/helpers/listbox_helpers.rb b/spec/support/helpers/listbox_helpers.rb
index 7a734d2b097..a8a4c079e3c 100644
--- a/spec/support/helpers/listbox_helpers.rb
+++ b/spec/support/helpers/listbox_helpers.rb
@@ -14,6 +14,10 @@ module ListboxHelpers
find('.gl-new-dropdown-item', text: text, exact_text: exact_text).click
end
+ def toggle_listbox
+ find('.gl-new-dropdown-toggle').click
+ end
+
def expect_listbox_item(text)
expect(page).to have_css('.gl-new-dropdown-item[role="option"]', text: text)
end
diff --git a/spec/support/helpers/login_helpers.rb b/spec/support/helpers/login_helpers.rb
index abe21d2b74c..d35fa801638 100644
--- a/spec/support/helpers/login_helpers.rb
+++ b/spec/support/helpers/login_helpers.rb
@@ -70,7 +70,13 @@ module LoginHelpers
# Requires Javascript driver.
def gitlab_sign_out
- find(".header-user-dropdown-toggle").click
+ if has_testid?('super-sidebar')
+ click_on "#{@current_user.name} user’s menu"
+ else
+ # This can be removed once https://gitlab.com/gitlab-org/gitlab/-/issues/420121 is complete.
+ find(".header-user-dropdown-toggle").click
+ end
+
click_link "Sign out"
@current_user = nil
@@ -79,11 +85,8 @@ module LoginHelpers
# Requires Javascript driver.
def gitlab_disable_admin_mode
- open_top_nav
-
- within_top_nav do
- click_on 'Leave admin mode'
- end
+ click_on 'Search or go to…'
+ click_on 'Leave admin mode'
end
private
@@ -209,9 +212,9 @@ module LoginHelpers
def mock_saml_config_with_upstream_two_factor_authn_contexts
config = mock_saml_config
- config.args[:upstream_two_factor_authn_contexts] = %w(urn:oasis:names:tc:SAML:2.0:ac:classes:CertificateProtectedTransport
+ config.args[:upstream_two_factor_authn_contexts] = %w[urn:oasis:names:tc:SAML:2.0:ac:classes:CertificateProtectedTransport
urn:oasis:names:tc:SAML:2.0:ac:classes:SecondFactorOTPSMS
- urn:oasis:names:tc:SAML:2.0:ac:classes:SecondFactorIGTOKEN)
+ urn:oasis:names:tc:SAML:2.0:ac:classes:SecondFactorIGTOKEN]
config
end
@@ -259,19 +262,15 @@ module LoginHelpers
end
def stub_omniauth_config(messages)
- allow(Gitlab.config.omniauth).to receive_messages(messages)
+ allow(Gitlab.config.omniauth).to receive_messages(GitlabSettings::Options.build(messages))
end
def stub_basic_saml_config
- allow_next_instance_of(Gitlab::Auth::Saml::Config) do |config|
- allow(config).to receive_messages({ options: { name: 'saml', args: {} } })
- end
+ stub_omniauth_config(providers: [{ name: 'saml', args: {} }])
end
def stub_saml_group_config(groups)
- allow_next_instance_of(Gitlab::Auth::Saml::Config) do |config|
- allow(config).to receive_messages({ options: { name: 'saml', groups_attribute: 'groups', external_groups: groups, args: {} } })
- end
+ stub_omniauth_config(providers: [{ name: 'saml', groups_attribute: 'groups', external_groups: groups, args: {} }])
end
end
diff --git a/spec/support/helpers/navbar_structure_helper.rb b/spec/support/helpers/navbar_structure_helper.rb
index fe39968b002..131c7597827 100644
--- a/spec/support/helpers/navbar_structure_helper.rb
+++ b/spec/support/helpers/navbar_structure_helper.rb
@@ -8,6 +8,13 @@ module NavbarStructureHelper
structure.insert(index + 1, new_nav_item)
end
+ def insert_before_nav_item(after_nav_item_name, new_nav_item:)
+ expect(structure).to include(a_hash_including(nav_item: after_nav_item_name))
+
+ index = structure.find_index { |h| h[:nav_item] == after_nav_item_name if h }
+ structure.insert(index, new_nav_item)
+ end
+
def insert_after_sub_nav_item(before_sub_nav_item_name, within:, new_sub_nav_item_name:)
expect(structure).to include(a_hash_including(nav_item: within))
hash = structure.find { |h| h[:nav_item] == within if h }
@@ -30,49 +37,57 @@ module NavbarStructureHelper
hash[:nav_sub_items].insert(index, new_sub_nav_item_name)
end
- def insert_package_nav(within)
- insert_after_nav_item(
- within,
- new_nav_item: {
- nav_item: _('Packages and registries'),
- nav_sub_items: [_('Package Registry')]
- }
+ def insert_package_nav
+ insert_after_sub_nav_item(
+ _("Feature flags"),
+ within: _('Deploy'),
+ new_sub_nav_item_name: _("Package Registry")
)
end
- def insert_customer_relations_nav(within)
- insert_after_nav_item(
- within,
+ def create_package_nav(before)
+ insert_before_nav_item(
+ before,
new_nav_item: {
- nav_item: _('Customer relations'),
- nav_sub_items: [
- _('Contacts'),
- _('Organizations')
- ]
+ nav_item: _("Deploy"),
+ nav_sub_items: [_("Package Registry")]
}
)
end
+ def insert_customer_relations_nav(after)
+ insert_after_sub_nav_item(
+ after,
+ within: _('Plan'),
+ new_sub_nav_item_name: _("Customer contacts")
+ )
+ insert_after_sub_nav_item(
+ _("Customer contacts"),
+ within: _('Plan'),
+ new_sub_nav_item_name: _("Customer organizations")
+ )
+ end
+
def insert_container_nav
insert_after_sub_nav_item(
_('Package Registry'),
- within: _('Packages and registries'),
+ within: _('Deploy'),
new_sub_nav_item_name: _('Container Registry')
)
end
def insert_dependency_proxy_nav
- insert_after_sub_nav_item(
- _('Package Registry'),
- within: _('Packages and registries'),
+ insert_before_sub_nav_item(
+ _('Kubernetes'),
+ within: _('Operate'),
new_sub_nav_item_name: _('Dependency Proxy')
)
end
def insert_infrastructure_registry_nav
insert_after_sub_nav_item(
- _('Package Registry'),
- within: _('Packages and registries'),
+ s_('Terraform|Terraform states'),
+ within: _('Operate'),
new_sub_nav_item_name: _('Terraform modules')
)
end
@@ -80,15 +95,15 @@ module NavbarStructureHelper
def insert_harbor_registry_nav(within)
insert_after_sub_nav_item(
within,
- within: _('Packages and registries'),
+ within: _('Operate'),
new_sub_nav_item_name: _('Harbor Registry')
)
end
def insert_infrastructure_google_cloud_nav
insert_after_sub_nav_item(
- s_('Terraform|Terraform states'),
- within: _('Infrastructure'),
+ s_('Terraform|Terraform modules'),
+ within: _('Operate'),
new_sub_nav_item_name: _('Google Cloud')
)
end
@@ -96,7 +111,7 @@ module NavbarStructureHelper
def insert_infrastructure_aws_nav
insert_after_sub_nav_item(
_('Google Cloud'),
- within: _('Infrastructure'),
+ within: _('Operate'),
new_sub_nav_item_name: _('AWS')
)
end
@@ -104,25 +119,24 @@ module NavbarStructureHelper
def insert_model_experiments_nav(within)
insert_after_sub_nav_item(
within,
- within: _('Packages and registries'),
+ within: _('Analyze'),
new_sub_nav_item_name: _('Model experiments')
)
end
def project_analytics_sub_nav_item
[
- _('Value stream'),
- _('CI/CD'),
- (_('Code review') if Gitlab.ee?),
- (_('Merge request') if Gitlab.ee?),
- _('Repository')
+ _('Value stream analytics'),
+ _('Contributor statistics'),
+ _('CI/CD analytics'),
+ _('Repository analytics'),
+ (_('Code review analytics') if Gitlab.ee?),
+ (_('Merge request analytics') if Gitlab.ee?)
]
end
def group_analytics_sub_nav_item
- [
- _('Contribution')
- ]
+ [_("Contribution analytics")]
end
end
diff --git a/spec/support/helpers/packages_manager_api_spec_helper.rb b/spec/support/helpers/packages_manager_api_spec_helpers.rb
index 1c9fce183e9..c81c9b5982e 100644
--- a/spec/support/helpers/packages_manager_api_spec_helper.rb
+++ b/spec/support/helpers/packages_manager_api_spec_helpers.rb
@@ -22,12 +22,12 @@ module PackagesManagerApiSpecHelpers
end
end
- def temp_file(package_tmp)
+ def temp_file(package_tmp, content: nil)
upload_path = ::Packages::PackageFileUploader.workhorse_local_upload_path
file_path = "#{upload_path}/#{package_tmp}"
FileUtils.mkdir_p(upload_path)
- File.write(file_path, 'test')
+ content ? FileUtils.cp(content, file_path) : File.write(file_path, 'test')
UploadedFile.new(file_path, filename: File.basename(file_path))
end
diff --git a/spec/support/helpers/prevent_set_operator_mismatch_helper.rb b/spec/support/helpers/prevent_set_operator_mismatch_helper.rb
new file mode 100644
index 00000000000..482a5560fe9
--- /dev/null
+++ b/spec/support/helpers/prevent_set_operator_mismatch_helper.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+module PreventSetOperatorMismatchHelper
+ extend ActiveSupport::Concern
+
+ included do
+ before do
+ stub_const('Type', Gitlab::Database::QueryAnalyzers::PreventSetOperatorMismatch::Type)
+ end
+ end
+
+ def sql_select_node(sql)
+ parsed = PgQuery.parse(sql)
+ parsed.tree.stmts[0].stmt.select_stmt
+ end
+end
diff --git a/spec/support/helpers/project_template_test_helper.rb b/spec/support/helpers/project_template_test_helper.rb
index 35e40faeea7..a02cd491bca 100644
--- a/spec/support/helpers/project_template_test_helper.rb
+++ b/spec/support/helpers/project_template_test_helper.rb
@@ -10,6 +10,7 @@ module ProjectTemplateTestHelper
serverless_framework tencent_serverless_framework
jsonnet cluster_management kotlin_native_linux
pelican bridgetown typo3_distribution laravel
+ astro_tailwind
]
end
end
diff --git a/spec/support/helpers/prometheus_helpers.rb b/spec/support/helpers/prometheus_helpers.rb
index da80f6f08c2..065c653c62f 100644
--- a/spec/support/helpers/prometheus_helpers.rb
+++ b/spec/support/helpers/prometheus_helpers.rb
@@ -207,7 +207,7 @@ module PrometheusHelpers
def prometheus_label_values
{
'status': 'success',
- 'data': %w(job_adds job_controller_rate_limiter_use job_depth job_queue_latency job_work_duration_sum up)
+ 'data': %w[job_adds job_controller_rate_limiter_use job_depth job_queue_latency job_work_duration_sum up]
}
end
diff --git a/spec/support/helpers/repo_helpers.rb b/spec/support/helpers/repo_helpers.rb
index d264356aa64..bac88da4885 100644
--- a/spec/support/helpers/repo_helpers.rb
+++ b/spec/support/helpers/repo_helpers.rb
@@ -112,13 +112,13 @@ eos
}
] + extra_changes
- commits = %w(
+ commits = %w[
5937ac0a7beb003549fc5fd26fc247adbce4a52e
570e7b2abdd848b95f2f578043fc23bd6f6fd24d
6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
d14d6c0abdd253381df51a723d58691b2ee1ab08
c1acaa58bbcbc3eafe538cb8274ba387047b69f8
- ).reverse # last commit is recent one
+ ].reverse # last commit is recent one
reviewers = [
{
diff --git a/spec/support/helpers/search_helpers.rb b/spec/support/helpers/search_helpers.rb
index d13703776cd..dd5ce63876e 100644
--- a/spec/support/helpers/search_helpers.rb
+++ b/spec/support/helpers/search_helpers.rb
@@ -2,16 +2,28 @@
module SearchHelpers
def fill_in_search(text)
- page.within('.header-search') do
- find('#search').click
- fill_in 'search', with: text
+ within_testid('super-sidebar') do
+ click_button "Search or go to…"
end
+ fill_in 'search', with: text
wait_for_all_requests
end
def submit_search(query)
- page.within('.header-search-form, .search-page-form') do
+ # Forms directly on the search page
+ if page.has_css?('.search-page-form')
+ search_form = '.search-page-form'
+ # Open search modal from super sidebar
+ elsif has_testid?('super-sidebar-search-button')
+ find_by_testid('super-sidebar-search-button').click
+ search_form = '#super-sidebar-search-modal'
+ # Open legacy search dropdown in navigation
+ else
+ search_form = '.header-search-form'
+ end
+
+ page.within(search_form) do
field = find_field('search')
field.click
field.fill_in(with: query)
@@ -27,7 +39,7 @@ module SearchHelpers
end
def select_search_scope(scope)
- page.within '[data-testid="search-filter"]' do
+ within_testid('search-filter') do
click_link scope
wait_for_all_requests
@@ -35,9 +47,9 @@ module SearchHelpers
end
def has_search_scope?(scope)
- return false unless page.has_selector?('[data-testid="search-filter"]')
+ return false unless has_testid?('search-filter')
- page.within '[data-testid="search-filter"]' do
+ within_testid('search-filter') do
has_link?(scope)
end
end
diff --git a/spec/support/helpers/seed_repo.rb b/spec/support/helpers/seed_repo.rb
index 74ac529a3de..b0bd0dfb60e 100644
--- a/spec/support/helpers/seed_repo.rb
+++ b/spec/support/helpers/seed_repo.rb
@@ -47,7 +47,7 @@ module SeedRepo
FILES_COUNT = 2
C_FILE_PATH = "files/ruby"
C_FILES = ["popen.rb", "regex.rb", "version_info.rb"].freeze
- BLOB_FILE = %{%h3= @key.title\n%hr\n%pre= @key.key\n.actions\n = link_to 'Remove', @key, :confirm => 'Are you sure?', :method => :delete, :class => \"btn danger delete-key\"\n\n\n}
+ BLOB_FILE = %(%h3= @key.title\n%hr\n%pre= @key.key\n.actions\n = link_to 'Remove', @key, :confirm => 'Are you sure?', :method => :delete, :class => \"btn danger delete-key\"\n\n\n)
BLOB_FILE_PATH = "app/views/keys/show.html.haml"
end
diff --git a/spec/support/helpers/stub_saas_features.rb b/spec/support/helpers/stub_saas_features.rb
index e344888cb8c..d0aa7108a6a 100644
--- a/spec/support/helpers/stub_saas_features.rb
+++ b/spec/support/helpers/stub_saas_features.rb
@@ -6,9 +6,9 @@ module StubSaasFeatures
# @param [Hash] features where key is feature name and value is boolean whether enabled or not.
#
# Examples
- # - `stub_saas_features('onboarding' => false)` ... Disable `onboarding`
+ # - `stub_saas_features(onboarding: false)` ... Disable `onboarding`
# SaaS feature globally.
- # - `stub_saas_features('onboarding' => true)` ... Enable `onboarding`
+ # - `stub_saas_features(onboarding: true)` ... Enable `onboarding`
# SaaS feature globally.
def stub_saas_features(features)
features.each do |feature_name, value|
diff --git a/spec/support/helpers/test_env.rb b/spec/support/helpers/test_env.rb
index 740abdb6cfa..e606a377ec7 100644
--- a/spec/support/helpers/test_env.rb
+++ b/spec/support/helpers/test_env.rb
@@ -305,7 +305,7 @@ module TestEnv
unless File.directory?(repo_path)
start = Time.now
- system(*%W(#{Gitlab.config.git.bin_path} clone --quiet -- #{clone_url} #{repo_path}))
+ system(*%W[#{Gitlab.config.git.bin_path} clone --quiet -- #{clone_url} #{repo_path}])
puts "==> #{repo_path} set up in #{Time.now - start} seconds...\n"
end
@@ -316,7 +316,7 @@ module TestEnv
# set all the required local branches. This would happen when a new
# branch is added to BRANCH_SHA, in which case we want to update
# everything.
- unless system(*%W(#{Gitlab.config.git.bin_path} -C #{repo_path} fetch origin))
+ unless system(*%W[#{Gitlab.config.git.bin_path} -C #{repo_path} fetch origin])
raise 'Could not fetch test seed repository.'
end
@@ -329,7 +329,7 @@ module TestEnv
if create_bundle
start = Time.now
- system(git_env, *%W(#{Gitlab.config.git.bin_path} -C #{repo_path} bundle create #{repo_bundle_path} --exclude refs/remotes/* --all))
+ system(git_env, *%W[#{Gitlab.config.git.bin_path} -C #{repo_path} bundle create #{repo_bundle_path} --exclude refs/remotes/* --all])
puts "==> #{repo_bundle_path} generated in #{Time.now - start} seconds...\n"
end
end
@@ -530,7 +530,7 @@ module TestEnv
return false unless Dir.exist?(component_folder)
- sha, exit_status = Gitlab::Popen.popen(%W(#{Gitlab.config.git.bin_path} rev-parse HEAD), component_folder)
+ sha, exit_status = Gitlab::Popen.popen(%W[#{Gitlab.config.git.bin_path} rev-parse HEAD], component_folder)
return false if exit_status != 0
expected_version == sha.chomp
diff --git a/spec/support/helpers/usage_data_helpers.rb b/spec/support/helpers/usage_data_helpers.rb
index 42e599c7510..3b8c0b42fe8 100644
--- a/spec/support/helpers/usage_data_helpers.rb
+++ b/spec/support/helpers/usage_data_helpers.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module UsageDataHelpers
- COUNTS_KEYS = %i(
+ COUNTS_KEYS = %i[
assignee_lists
ci_builds
ci_external_pipelines
@@ -72,9 +72,9 @@ module UsageDataHelpers
uploads
web_hooks
user_preferences_user_gitpod_enabled
- ).freeze
+ ].freeze
- USAGE_DATA_KEYS = %i(
+ USAGE_DATA_KEYS = %i[
counts
recorded_at
mattermost_enabled
@@ -93,7 +93,7 @@ module UsageDataHelpers
prometheus_metrics_enabled
object_store
topology
- ).freeze
+ ].freeze
def stub_usage_data_connections
Gitlab::Database.database_base_models_with_gitlab_shared.each_value do |base_model|
diff --git a/spec/support/import_export/configuration_helper.rb b/spec/support/import_export/configuration_helper.rb
index 28797229661..f6917cdd89e 100644
--- a/spec/support/import_export/configuration_helper.rb
+++ b/spec/support/import_export/configuration_helper.rb
@@ -32,7 +32,7 @@ module ConfigurationHelper
# - project is not part of the tree, so it has to be added manually.
# - milestone, labels, merge_request have both singular and plural versions in the tree, so remove the duplicates.
# - User, Author... Models we do not care about for checking models
- names.flatten.uniq - %w(milestones labels user author merge_request design) + [key.to_s]
+ names.flatten.uniq - %w[milestones labels user author merge_request design] + [key.to_s]
end
def relation_class_for_name(relation_name)
diff --git a/spec/support/import_export/export_file_helper.rb b/spec/support/import_export/export_file_helper.rb
index 3be2d39906d..8068d3082d5 100644
--- a/spec/support/import_export/export_file_helper.rb
+++ b/spec/support/import_export/export_file_helper.rb
@@ -59,7 +59,7 @@ module ExportFileHelper
def in_directory_with_expanded_export(project)
Dir.mktmpdir do |tmpdir|
export_file = project.export_file.path
- _output, exit_status = Gitlab::Popen.popen(%W{tar -zxf #{export_file} -C #{tmpdir}})
+ _output, exit_status = Gitlab::Popen.popen(%W[tar -zxf #{export_file} -C #{tmpdir}])
yield(exit_status, tmpdir)
end
diff --git a/spec/support/matchers/markdown_matchers.rb b/spec/support/matchers/markdown_matchers.rb
index 7a82d7674d9..21fc1824bea 100644
--- a/spec/support/matchers/markdown_matchers.rb
+++ b/spec/support/matchers/markdown_matchers.rb
@@ -76,7 +76,7 @@ module MarkdownMatchers
expect(actual).to have_autolink('irc://irc.freenode.net/git')
expect(actual).to have_autolink('http://localhost:3000')
- %w(code a kbd).each do |elem|
+ %w[code a kbd].each do |elem|
expect(body).not_to have_selector("#{elem} a")
end
end
diff --git a/spec/support/matchers/navigation_matcher.rb b/spec/support/matchers/navigation_matcher.rb
index a0beecbfb2c..400c2fe7436 100644
--- a/spec/support/matchers/navigation_matcher.rb
+++ b/spec/support/matchers/navigation_matcher.rb
@@ -1,14 +1,20 @@
# frozen_string_literal: true
+# These matches look for selectors within the Vue navigation sidebar.
+# They should therefore be used in feature specs with the Js driver enabled.
+
RSpec::Matchers.define :have_active_navigation do |expected|
match do |page|
- expect(page).to have_selector('.sidebar-top-level-items > li.active', count: 1)
- expect(page.find('.sidebar-top-level-items > li.active')).to have_content(expected)
+ within_testid('super-sidebar') do
+ expect(page).to have_selector('button[aria-expanded="true"]', text: expected)
+ end
end
end
RSpec::Matchers.define :have_active_sub_navigation do |expected|
match do |page|
- expect(page).to have_css('.sidebar-sub-level-items > li.active:not(.fly-out-top-item)', text: expected)
+ within_testid('super-sidebar') do
+ expect(page).to have_selector('[aria-current="page"]', text: expected)
+ end
end
end
diff --git a/spec/support/redis.rb b/spec/support/redis.rb
index d5ae0bf1582..9cf5c44de98 100644
--- a/spec/support/redis.rb
+++ b/spec/support/redis.rb
@@ -3,9 +3,8 @@ require 'gitlab/redis'
RSpec.configure do |config|
config.after(:each, :redis) do
- Sidekiq.redis do |connection|
- connection.redis.flushdb
- end
+ Sidekiq.redis(&:flushdb)
+ redis_queues_metadata_cleanup!
end
Gitlab::Redis::ALL_CLASSES.each do |instance_class|
@@ -13,10 +12,12 @@ RSpec.configure do |config|
config.around(:each, :"clean_gitlab_redis_#{underscored_name}") do |example|
public_send("redis_#{underscored_name}_cleanup!")
+ redis_queues_metadata_cleanup! if underscored_name == 'queues'
example.run
public_send("redis_#{underscored_name}_cleanup!")
+ redis_queues_metadata_cleanup! if underscored_name == 'queues'
end
end
diff --git a/spec/support/rspec_order_todo.yml b/spec/support/rspec_order_todo.yml
index 51f3ff2c077..da23f81e86e 100644
--- a/spec/support/rspec_order_todo.yml
+++ b/spec/support/rspec_order_todo.yml
@@ -81,7 +81,6 @@
- './ee/spec/controllers/groups/iteration_cadences_controller_spec.rb'
- './ee/spec/controllers/groups/iterations_controller_spec.rb'
- './ee/spec/controllers/groups/ldaps_controller_spec.rb'
-- './ee/spec/controllers/groups/ldap_settings_controller_spec.rb'
- './ee/spec/controllers/groups/merge_requests_controller_spec.rb'
- './ee/spec/controllers/groups/omniauth_callbacks_controller_spec.rb'
- './ee/spec/controllers/groups/push_rules_controller_spec.rb'
@@ -132,7 +131,6 @@
- './ee/spec/controllers/projects/merge_requests_controller_spec.rb'
- './ee/spec/controllers/projects/merge_requests/creations_controller_spec.rb'
- './ee/spec/controllers/projects/mirrors_controller_spec.rb'
-- './ee/spec/controllers/projects/pages_controller_spec.rb'
- './ee/spec/controllers/projects/path_locks_controller_spec.rb'
- './ee/spec/controllers/projects/pipelines_controller_spec.rb'
- './ee/spec/controllers/projects/protected_environments_controller_spec.rb'
@@ -165,7 +163,6 @@
- './ee/spec/controllers/users_controller_spec.rb'
- './ee/spec/db/production/license_spec.rb'
- './ee/spec/elastic_integration/global_search_spec.rb'
-- './ee/spec/elastic_integration/repository_index_spec.rb'
- './ee/spec/elastic/migrate/20201105181100_apply_max_analyzed_offset_spec.rb'
- './ee/spec/elastic/migrate/20201116142400_add_new_data_to_issues_documents_spec.rb'
- './ee/spec/elastic/migrate/20201123123400_migrate_issues_to_separate_index_spec.rb'
@@ -203,7 +200,6 @@
- './ee/spec/features/admin/admin_show_new_user_signups_cap_alert_spec.rb'
- './ee/spec/features/admin/admin_users_spec.rb'
- './ee/spec/features/admin/geo/admin_geo_nodes_spec.rb'
-- './ee/spec/features/admin/geo/admin_geo_projects_spec.rb'
- './ee/spec/features/admin/geo/admin_geo_replication_nav_spec.rb'
- './ee/spec/features/admin/geo/admin_geo_sidebar_spec.rb'
- './ee/spec/features/admin/groups/admin_changes_plan_spec.rb'
@@ -292,7 +288,6 @@
- './ee/spec/features/groups/iterations/user_views_iteration_cadence_spec.rb'
- './ee/spec/features/groups/iterations/user_views_iteration_spec.rb'
- './ee/spec/features/groups/ldap_group_links_spec.rb'
-- './ee/spec/features/groups/ldap_settings_spec.rb'
- './ee/spec/features/groups/members/leave_group_spec.rb'
- './ee/spec/features/groups/members/list_members_spec.rb'
- './ee/spec/features/groups/members/manage_groups_spec.rb'
@@ -337,7 +332,6 @@
- './ee/spec/features/issues/user_views_issues_spec.rb'
- './ee/spec/features/labels_hierarchy_spec.rb'
- './ee/spec/features/markdown/markdown_spec.rb'
-- './ee/spec/features/markdown/metrics_spec.rb'
- './ee/spec/features/merge_request/merge_request_widget_blocking_mrs_spec.rb'
- './ee/spec/features/merge_request/sidebar_spec.rb'
- './ee/spec/features/merge_requests/user_filters_by_approvers_spec.rb'
@@ -393,7 +387,6 @@
- './ee/spec/features/projects/insights_spec.rb'
- './ee/spec/features/projects/integrations/jira_issues_list_spec.rb'
- './ee/spec/features/projects/integrations/project_integrations_spec.rb'
-- './ee/spec/features/projects/integrations/prometheus_custom_metrics_spec.rb'
- './ee/spec/features/projects/integrations/user_activates_github_spec.rb'
- './ee/spec/features/projects/integrations/user_activates_jira_spec.rb'
- './ee/spec/features/projects/issues/user_creates_issue_spec.rb'
@@ -476,7 +469,6 @@
- './ee/spec/features/security/project/snippet/public_access_spec.rb'
- './ee/spec/features/signup_spec.rb'
- './ee/spec/features/subscriptions/expiring_subscription_message_spec.rb'
-- './ee/spec/features/subscriptions/groups/edit_spec.rb'
- './ee/spec/features/subscriptions_spec.rb'
- './ee/spec/features/trial_registrations/company_information_spec.rb'
- './ee/spec/features/trial_registrations/signin_spec.rb'
@@ -509,7 +501,6 @@
- './ee/spec/finders/dast_site_profiles_finder_spec.rb'
- './ee/spec/finders/dast_site_validations_finder_spec.rb'
- './ee/spec/finders/ee/alert_management/http_integrations_finder_spec.rb'
-- './ee/spec/finders/ee/autocomplete/users_finder_spec.rb'
- './ee/spec/finders/ee/ci/daily_build_group_report_results_finder_spec.rb'
- './ee/spec/finders/ee/clusters/agents_finder_spec.rb'
- './ee/spec/finders/ee/fork_targets_finder_spec.rb'
@@ -527,9 +518,6 @@
- './ee/spec/finders/geo/package_file_registry_finder_spec.rb'
- './ee/spec/finders/geo/pages_deployment_registry_finder_spec.rb'
- './ee/spec/finders/geo/pipeline_artifact_registry_finder_spec.rb'
-- './ee/spec/finders/geo/project_registry_finder_spec.rb'
-- './ee/spec/finders/geo/project_registry_status_finder_spec.rb'
-- './ee/spec/finders/geo/repository_verification_finder_spec.rb'
- './ee/spec/finders/geo/snippet_repository_registry_finder_spec.rb'
- './ee/spec/finders/geo/terraform_state_version_registry_finder_spec.rb'
- './ee/spec/finders/geo/upload_registry_finder_spec.rb'
@@ -552,15 +540,10 @@
- './ee/spec/finders/security/findings_finder_spec.rb'
- './ee/spec/finders/security/pipeline_vulnerabilities_finder_spec.rb'
- './ee/spec/finders/security/scan_execution_policies_finder_spec.rb'
-- './ee/spec/finders/security/training_providers/base_url_finder_spec.rb'
-- './ee/spec/finders/security/training_providers/kontra_url_finder_spec.rb'
-- './ee/spec/finders/security/training_providers/secure_code_warrior_url_finder_spec.rb'
-- './ee/spec/finders/security/training_urls_finder_spec.rb'
- './ee/spec/finders/security/vulnerabilities_finder_spec.rb'
- './ee/spec/finders/security/vulnerability_feedbacks_finder_spec.rb'
- './ee/spec/finders/security/vulnerability_reads_finder_spec.rb'
- './ee/spec/finders/snippets_finder_spec.rb'
-- './ee/spec/finders/software_license_policies_finder_spec.rb'
- './ee/spec/finders/template_finder_spec.rb'
- './ee/spec/finders/users_finder_spec.rb'
- './ee/spec/frontend/fixtures/analytics/charts.rb'
@@ -606,11 +589,9 @@
- './ee/spec/graphql/ee/types/mutation_type_spec.rb'
- './ee/spec/graphql/ee/types/namespace_type_spec.rb'
- './ee/spec/graphql/ee/types/notes/noteable_interface_spec.rb'
-- './ee/spec/graphql/ee/types/projects/service_type_enum_spec.rb'
- './ee/spec/graphql/ee/types/repository/blob_type_spec.rb'
- './ee/spec/graphql/ee/types/todoable_interface_spec.rb'
- './ee/spec/graphql/ee/types/user_merge_request_interaction_type_spec.rb'
-- './ee/spec/graphql/mutations/app_sec/fuzzing/api/ci_configuration/create_spec.rb'
- './ee/spec/graphql/mutations/app_sec/fuzzing/coverage/corpus/create_spec.rb'
- './ee/spec/graphql/mutations/audit_events/streaming/headers/create_spec.rb'
- './ee/spec/graphql/mutations/audit_events/streaming/headers/destroy_spec.rb'
@@ -671,7 +652,6 @@
- './ee/spec/graphql/mutations/security_policy/commit_scan_execution_policy_spec.rb'
- './ee/spec/graphql/mutations/security_policy/create_security_policy_project_spec.rb'
- './ee/spec/graphql/mutations/security_policy/unassign_security_policy_project_spec.rb'
-- './ee/spec/graphql/mutations/security/training_provider_update_spec.rb'
- './ee/spec/graphql/mutations/todos/create_spec.rb'
- './ee/spec/graphql/mutations/vulnerabilities/confirm_spec.rb'
- './ee/spec/graphql/mutations/vulnerabilities/create_external_issue_link_spec.rb'
@@ -727,7 +707,6 @@
- './ee/spec/graphql/resolvers/security_orchestration/scan_execution_policy_resolver_spec.rb'
- './ee/spec/graphql/resolvers/security_orchestration/scan_result_policy_resolver_spec.rb'
- './ee/spec/graphql/resolvers/security_report_summary_resolver_spec.rb'
-- './ee/spec/graphql/resolvers/security_training_urls_resolver_spec.rb'
- './ee/spec/graphql/resolvers/timebox_report_resolver_spec.rb'
- './ee/spec/graphql/resolvers/user_discussions_count_resolver_spec.rb'
- './ee/spec/graphql/resolvers/user_notes_count_resolver_spec.rb'
@@ -961,7 +940,6 @@
- './ee/spec/helpers/ee/system_note_helper_spec.rb'
- './ee/spec/helpers/ee/todos_helper_spec.rb'
- './ee/spec/helpers/ee/users/callouts_helper_spec.rb'
-- './ee/spec/helpers/ee/version_check_helper_spec.rb'
- './ee/spec/helpers/ee/wiki_helper_spec.rb'
- './ee/spec/helpers/epics_helper_spec.rb'
- './ee/spec/helpers/gitlab_subscriptions/upcoming_reconciliation_helper_spec.rb'
@@ -1019,7 +997,6 @@
- './ee/spec/lib/audit/details_spec.rb'
- './ee/spec/lib/audit/external_status_check_changes_auditor_spec.rb'
- './ee/spec/lib/audit/group_merge_request_approval_setting_changes_auditor_spec.rb'
-- './ee/spec/lib/audit/group_push_rules_changes_auditor_spec.rb'
- './ee/spec/lib/banzai/filter/cross_project_issuable_information_filter_spec.rb'
- './ee/spec/lib/banzai/filter/issuable_reference_expansion_filter_spec.rb'
- './ee/spec/lib/banzai/filter/jira_private_image_link_filter_spec.rb'
@@ -1092,14 +1069,11 @@
- './ee/spec/lib/ee/gitlab/auth/saml/identity_linker_spec.rb'
- './ee/spec/lib/ee/gitlab/background_migration/backfill_iteration_cadence_id_for_boards_spec.rb'
- './ee/spec/lib/ee/gitlab/background_migration/backfill_project_statistics_container_repository_size_spec.rb'
-- './ee/spec/lib/ee/gitlab/background_migration/create_security_setting_spec.rb'
- './ee/spec/lib/ee/gitlab/background_migration/delete_invalid_epic_issues_spec.rb'
- './ee/spec/lib/ee/gitlab/background_migration/migrate_approver_to_approval_rules_check_progress_spec.rb'
- './ee/spec/lib/ee/gitlab/background_migration/migrate_approver_to_approval_rules_in_batch_spec.rb'
- './ee/spec/lib/ee/gitlab/background_migration/migrate_approver_to_approval_rules_spec.rb'
- './ee/spec/lib/ee/gitlab/background_migration/migrate_shared_vulnerability_scanners_spec.rb'
-- './ee/spec/lib/ee/gitlab/background_migration/populate_latest_pipeline_ids_spec.rb'
-- './ee/spec/lib/ee/gitlab/background_migration/populate_resolved_on_default_branch_column_spec.rb'
- './ee/spec/lib/ee/gitlab/background_migration/purge_stale_security_scans_spec.rb'
- './ee/spec/lib/ee/gitlab/checks/push_rule_check_spec.rb'
- './ee/spec/lib/ee/gitlab/checks/push_rules/branch_check_spec.rb'
@@ -1118,14 +1092,11 @@
- './ee/spec/lib/ee/gitlab/ci/pipeline/chain/validate/after_config_spec.rb'
- './ee/spec/lib/ee/gitlab/ci/pipeline/chain/validate/external_spec.rb'
- './ee/spec/lib/ee/gitlab/ci/pipeline/chain/validate/security_orchestration_policy_spec.rb'
-- './ee/spec/lib/ee/gitlab/ci/pipeline/quota/activity_spec.rb'
- './ee/spec/lib/ee/gitlab/ci/pipeline/quota/size_spec.rb'
-- './ee/spec/lib/ee/gitlab/ci/reports/security/reports_spec.rb'
- './ee/spec/lib/ee/gitlab/ci/status/build/manual_spec.rb'
- './ee/spec/lib/ee/gitlab/ci/templates/templates_spec.rb'
- './ee/spec/lib/ee/gitlab/cleanup/orphan_job_artifact_files_batch_spec.rb'
- './ee/spec/lib/ee/gitlab/cleanup/orphan_job_artifact_files_spec.rb'
-- './ee/spec/lib/ee/gitlab/database/gitlab_schema_spec.rb'
- './ee/spec/lib/ee/gitlab/database_spec.rb'
- './ee/spec/lib/ee/gitlab/elastic/helper_spec.rb'
- './ee/spec/lib/ee/gitlab/email/handler/service_desk_handler_spec.rb'
@@ -1150,11 +1121,9 @@
- './ee/spec/lib/ee/gitlab/issuable_metadata_spec.rb'
- './ee/spec/lib/ee/gitlab/metrics/samplers/database_sampler_spec.rb'
- './ee/spec/lib/ee/gitlab/middleware/read_only_spec.rb'
-- './ee/spec/lib/ee/gitlab/namespaces/storage/enforcement_spec.rb'
- './ee/spec/lib/ee/gitlab/namespace_storage_size_error_message_spec.rb'
- './ee/spec/lib/ee/gitlab/omniauth_initializer_spec.rb'
- './ee/spec/lib/ee/gitlab/pages/deployment_update_spec.rb'
-- './ee/spec/lib/ee/gitlab/prometheus/metric_group_spec.rb'
- './ee/spec/lib/ee/gitlab/rack_attack/request_spec.rb'
- './ee/spec/lib/ee/gitlab/repo_path_spec.rb'
- './ee/spec/lib/ee/gitlab/repository_size_checker_spec.rb'
@@ -1169,7 +1138,6 @@
- './ee/spec/lib/ee/gitlab/template/gitlab_ci_yml_template_spec.rb'
- './ee/spec/lib/ee/gitlab/tracking_spec.rb'
- './ee/spec/lib/ee/gitlab/url_builder_spec.rb'
-- './ee/spec/lib/ee/gitlab/usage_data_counters/hll_redis_counter_spec.rb'
- './ee/spec/lib/ee/gitlab/usage_data_counters/issue_activity_unique_counter_spec.rb'
- './ee/spec/lib/ee/gitlab/usage_data_counters/merge_request_activity_unique_counter_spec.rb'
- './ee/spec/lib/ee/gitlab/usage_data_counters/work_item_activity_unique_counter_spec.rb'
@@ -1300,7 +1268,6 @@
- './ee/spec/lib/gitlab/ci/parsers/security/validators/default_branch_image_validator_spec.rb'
- './ee/spec/lib/gitlab/ci/pipeline/chain/config/content_spec.rb'
- './ee/spec/lib/gitlab/ci/pipeline/chain/create_cross_database_associations_spec.rb'
-- './ee/spec/lib/gitlab/ci/pipeline/chain/limit/activity_spec.rb'
- './ee/spec/lib/gitlab/ci/pipeline/chain/limit/size_spec.rb'
- './ee/spec/lib/gitlab/ci/reports/coverage_fuzzing/report_spec.rb'
- './ee/spec/lib/gitlab/ci/reports/dependency_list/dependency_spec.rb'
@@ -1387,13 +1354,6 @@
- './ee/spec/lib/gitlab/geo/log_cursor/events/cache_invalidation_event_spec.rb'
- './ee/spec/lib/gitlab/geo/log_cursor/events/event_spec.rb'
- './ee/spec/lib/gitlab/geo/log_cursor/events/hashed_storage_attachments_event_spec.rb'
-- './ee/spec/lib/gitlab/geo/log_cursor/events/hashed_storage_migrated_event_spec.rb'
-- './ee/spec/lib/gitlab/geo/log_cursor/events/repositories_changed_event_spec.rb'
-- './ee/spec/lib/gitlab/geo/log_cursor/events/repository_created_event_spec.rb'
-- './ee/spec/lib/gitlab/geo/log_cursor/events/repository_deleted_event_spec.rb'
-- './ee/spec/lib/gitlab/geo/log_cursor/events/repository_renamed_event_spec.rb'
-- './ee/spec/lib/gitlab/geo/log_cursor/events/repository_updated_event_spec.rb'
-- './ee/spec/lib/gitlab/geo/log_cursor/events/reset_checksum_event_spec.rb'
- './ee/spec/lib/gitlab/geo/log_cursor/lease_spec.rb'
- './ee/spec/lib/gitlab/geo/log_cursor/logger_spec.rb'
- './ee/spec/lib/gitlab/geo/logger_spec.rb'
@@ -1461,12 +1421,8 @@
- './ee/spec/lib/gitlab/pagination/keyset/simple_order_builder_spec.rb'
- './ee/spec/lib/gitlab/patch/database_config_spec.rb'
- './ee/spec/lib/gitlab/patch/draw_route_spec.rb'
-- './ee/spec/lib/gitlab/patch/geo_database_tasks_spec.rb'
- './ee/spec/lib/gitlab/path_locks_finder_spec.rb'
- './ee/spec/lib/gitlab/project_template_spec.rb'
-- './ee/spec/lib/gitlab/prometheus/queries/additional_metrics_deployment_query_spec.rb'
-- './ee/spec/lib/gitlab/prometheus/queries/additional_metrics_environment_query_spec.rb'
-- './ee/spec/lib/gitlab/prometheus/queries/cluster_query_spec.rb'
- './ee/spec/lib/gitlab/proxy_spec.rb'
- './ee/spec/lib/gitlab/quick_actions/users_extractor_spec.rb'
- './ee/spec/lib/gitlab/rack_attack_spec.rb'
@@ -1523,7 +1479,6 @@
- './ee/spec/lib/gitlab/visibility_level_spec.rb'
- './ee/spec/lib/gitlab/vulnerabilities/base_vulnerability_spec.rb'
- './ee/spec/lib/gitlab/vulnerabilities/container_scanning_vulnerability_spec.rb'
-- './ee/spec/lib/gitlab/vulnerabilities/findings_preloader_spec.rb'
- './ee/spec/lib/gitlab/vulnerabilities/parser_spec.rb'
- './ee/spec/lib/gitlab/vulnerabilities/standard_vulnerability_spec.rb'
- './ee/spec/lib/gitlab/web_ide/config/entry/schema/match_spec.rb'
@@ -1552,7 +1507,6 @@
- './ee/spec/mailers/devise_mailer_spec.rb'
- './ee/spec/mailers/ee/emails/admin_notification_spec.rb'
- './ee/spec/mailers/ee/emails/issues_spec.rb'
-- './ee/spec/mailers/ee/emails/merge_requests_spec.rb'
- './ee/spec/mailers/ee/emails/profile_spec.rb'
- './ee/spec/mailers/ee/emails/projects_spec.rb'
- './ee/spec/mailers/emails/epics_spec.rb'
@@ -1576,7 +1530,6 @@
- './ee/spec/models/allowed_email_domain_spec.rb'
- './ee/spec/models/analytics/cycle_analytics/aggregation_context_spec.rb'
- './ee/spec/models/analytics/cycle_analytics/group_level_spec.rb'
-- './ee/spec/models/analytics/cycle_analytics/runtime_limiter_spec.rb'
- './ee/spec/models/analytics/devops_adoption/enabled_namespace_spec.rb'
- './ee/spec/models/analytics/devops_adoption/snapshot_spec.rb'
- './ee/spec/models/analytics/issues_analytics_spec.rb'
@@ -1634,7 +1587,6 @@
- './ee/spec/models/concerns/ee/project_security_scanners_information_spec.rb'
- './ee/spec/models/concerns/ee/weight_eventable_spec.rb'
- './ee/spec/models/concerns/elastic/application_versioned_search_spec.rb'
-- './ee/spec/models/concerns/elastic/issue_spec.rb'
- './ee/spec/models/concerns/elastic/merge_request_spec.rb'
- './ee/spec/models/concerns/elastic/milestone_spec.rb'
- './ee/spec/models/concerns/elastic/note_spec.rb'
@@ -1707,7 +1659,6 @@
- './ee/spec/models/ee/namespaces/namespace_ban_spec.rb'
- './ee/spec/models/ee/namespace_spec.rb'
- './ee/spec/models/ee/namespace_statistics_spec.rb'
-- './ee/spec/models/ee/namespace/storage/notification_spec.rb'
- './ee/spec/models/ee/notification_setting_spec.rb'
- './ee/spec/models/ee/pages_deployment_spec.rb'
- './ee/spec/models/ee/personal_access_token_spec.rb'
@@ -1718,7 +1669,6 @@
- './ee/spec/models/ee/project_setting_spec.rb'
- './ee/spec/models/ee/project_wiki_spec.rb'
- './ee/spec/models/ee/protected_branch_spec.rb'
-- './ee/spec/models/ee/protected_ref_access_spec.rb'
- './ee/spec/models/ee/protected_ref_spec.rb'
- './ee/spec/models/ee/release_spec.rb'
- './ee/spec/models/ee/resource_label_event_spec.rb'
@@ -1761,7 +1711,6 @@
- './ee/spec/models/geo/package_file_registry_spec.rb'
- './ee/spec/models/geo/pages_deployment_registry_spec.rb'
- './ee/spec/models/geo/pipeline_artifact_registry_spec.rb'
-- './ee/spec/models/geo/project_registry_spec.rb'
- './ee/spec/models/geo/push_user_spec.rb'
- './ee/spec/models/geo/repositories_changed_event_spec.rb'
- './ee/spec/models/geo/repository_created_event_spec.rb'
@@ -1794,7 +1743,6 @@
- './ee/spec/models/integrations/github_spec.rb'
- './ee/spec/models/integrations/github/status_message_spec.rb'
- './ee/spec/models/integrations/github/status_notifier_spec.rb'
-- './ee/spec/models/integrations/gitlab_slack_application_spec.rb'
- './ee/spec/models/ip_restriction_spec.rb'
- './ee/spec/models/issuable_metric_image_spec.rb'
- './ee/spec/models/issuables_analytics_spec.rb'
@@ -1833,7 +1781,6 @@
- './ee/spec/models/project_member_spec.rb'
- './ee/spec/models/project_repository_state_spec.rb'
- './ee/spec/models/project_security_setting_spec.rb'
-- './ee/spec/models/project_team_spec.rb'
- './ee/spec/models/protected_branch/required_code_owners_section_spec.rb'
- './ee/spec/models/protected_branch/unprotect_access_level_spec.rb'
- './ee/spec/models/protected_environments/approval_rule_spec.rb'
@@ -1863,7 +1810,6 @@
- './ee/spec/models/security/scan_spec.rb'
- './ee/spec/models/security/training_provider_spec.rb'
- './ee/spec/models/security/training_spec.rb'
-- './ee/spec/models/slack_integration_spec.rb'
- './ee/spec/models/snippet_repository_spec.rb'
- './ee/spec/models/snippet_spec.rb'
- './ee/spec/models/software_license_policy_spec.rb'
@@ -1927,7 +1873,6 @@
- './ee/spec/policies/global_policy_spec.rb'
- './ee/spec/policies/group_hook_policy_spec.rb'
- './ee/spec/policies/group_policy_spec.rb'
-- './ee/spec/policies/identity_provider_policy_spec.rb'
- './ee/spec/policies/instance_security_dashboard_policy_spec.rb'
- './ee/spec/policies/issuable_policy_spec.rb'
- './ee/spec/policies/issue_policy_spec.rb'
@@ -2002,7 +1947,6 @@
- './ee/spec/requests/api/award_emoji_spec.rb'
- './ee/spec/requests/api/boards_spec.rb'
- './ee/spec/requests/api/branches_spec.rb'
-- './ee/spec/requests/api/captcha_check_spec.rb'
- './ee/spec/requests/api/ci/jobs_spec.rb'
- './ee/spec/requests/api/ci/minutes_spec.rb'
- './ee/spec/requests/api/ci/pipelines_spec.rb'
@@ -2024,7 +1968,6 @@
- './ee/spec/requests/api/features_spec.rb'
- './ee/spec/requests/api/files_spec.rb'
- './ee/spec/requests/api/geo_nodes_spec.rb'
-- './ee/spec/requests/api/geo_replication_spec.rb'
- './ee/spec/requests/api/geo_spec.rb'
- './ee/spec/requests/api/graphql/analytics/devops_adoption/enabled_namespaces_spec.rb'
- './ee/spec/requests/api/graphql/app_sec/fuzzing/api/ci_configuration_type_spec.rb'
@@ -2071,7 +2014,6 @@
- './ee/spec/requests/api/graphql/mutations/analytics/devops_adoption/enabled_namespaces/bulk_enable_spec.rb'
- './ee/spec/requests/api/graphql/mutations/analytics/devops_adoption/enabled_namespaces/disable_spec.rb'
- './ee/spec/requests/api/graphql/mutations/analytics/devops_adoption/enabled_namespaces/enable_spec.rb'
-- './ee/spec/requests/api/graphql/mutations/app_sec/fuzzing/api/ci_configuration/create_spec.rb'
- './ee/spec/requests/api/graphql/mutations/audit_events/external_audit_event_destinations/create_spec.rb'
- './ee/spec/requests/api/graphql/mutations/audit_events/external_audit_event_destinations/destroy_spec.rb'
- './ee/spec/requests/api/graphql/mutations/audit_events/external_audit_event_destinations/update_spec.rb'
@@ -2142,7 +2084,6 @@
- './ee/spec/requests/api/graphql/mutations/users/abuse/namespace_bans/destroy_spec.rb'
- './ee/spec/requests/api/graphql/mutations/vulnerabilities/create_external_issue_link_spec.rb'
- './ee/spec/requests/api/graphql/mutations/vulnerabilities/destroy_external_issue_link_spec.rb'
-- './ee/spec/requests/api/graphql/mutations/vulnerabilities/finding_dismiss_spec.rb'
- './ee/spec/requests/api/graphql/mutations/work_items/update_spec.rb'
- './ee/spec/requests/api/graphql/namespace/compliance_frameworks_spec.rb'
- './ee/spec/requests/api/graphql/namespace/projects_spec.rb'
@@ -2174,7 +2115,6 @@
- './ee/spec/requests/api/graphql/project/security_orchestration/scan_result_policy_spec.rb'
- './ee/spec/requests/api/graphql/project/vulnerability_severities_count_spec.rb'
- './ee/spec/requests/api/graphql/project/work_items_spec.rb'
-- './ee/spec/requests/api/graphql/vulnerabilities/description_spec.rb'
- './ee/spec/requests/api/graphql/vulnerabilities/details_spec.rb'
- './ee/spec/requests/api/graphql/vulnerabilities/external_issue_links_spec.rb'
- './ee/spec/requests/api/graphql/vulnerabilities/identifiers_spec.rb'
@@ -2241,7 +2181,6 @@
- './ee/spec/requests/api/todos_spec.rb'
- './ee/spec/requests/api/usage_data_spec.rb'
- './ee/spec/requests/api/users_spec.rb'
-- './ee/spec/requests/api/v3/github_spec.rb'
- './ee/spec/requests/api/visual_review_discussions_spec.rb'
- './ee/spec/requests/api/vulnerabilities_spec.rb'
- './ee/spec/requests/api/vulnerability_exports_spec.rb'
@@ -2260,7 +2199,6 @@
- './ee/spec/requests/groups/analytics/devops_adoption_controller_spec.rb'
- './ee/spec/requests/groups/audit_events_spec.rb'
- './ee/spec/requests/groups/clusters_controller_spec.rb'
-- './ee/spec/requests/groups/compliance_frameworks_spec.rb'
- './ee/spec/requests/groups/contribution_analytics_spec.rb'
- './ee/spec/requests/groups_controller_spec.rb'
- './ee/spec/requests/groups/epics/epic_links_controller_spec.rb'
@@ -2358,7 +2296,6 @@
- './ee/spec/serializers/integrations/jira_serializers/issue_entity_spec.rb'
- './ee/spec/serializers/integrations/jira_serializers/issue_serializer_spec.rb'
- './ee/spec/serializers/integrations/zentao_serializers/issue_entity_spec.rb'
-- './ee/spec/serializers/issuable_sidebar_extras_entity_spec.rb'
- './ee/spec/serializers/issue_serializer_spec.rb'
- './ee/spec/serializers/issues/linked_issue_feature_flag_entity_spec.rb'
- './ee/spec/serializers/license_compliance/collapsed_comparer_entity_spec.rb'
@@ -2370,7 +2307,6 @@
- './ee/spec/serializers/member_entity_spec.rb'
- './ee/spec/serializers/member_user_entity_spec.rb'
- './ee/spec/serializers/merge_request_poll_widget_entity_spec.rb'
-- './ee/spec/serializers/merge_request_sidebar_basic_entity_spec.rb'
- './ee/spec/serializers/merge_request_widget_entity_spec.rb'
- './ee/spec/serializers/metrics_report_metric_entity_spec.rb'
- './ee/spec/serializers/metrics_reports_comparer_entity_spec.rb'
@@ -2454,10 +2390,8 @@
- './ee/spec/services/app_sec/fuzzing/coverage/corpuses/create_service_spec.rb'
- './ee/spec/services/arkose/blocked_users_report_service_spec.rb'
- './ee/spec/services/audit_events/build_service_spec.rb'
-- './ee/spec/services/audit_events/custom_audit_event_service_spec.rb'
- './ee/spec/services/audit_event_service_spec.rb'
- './ee/spec/services/audit_events/export_csv_service_spec.rb'
-- './ee/spec/services/audit_events/impersonation_audit_event_service_spec.rb'
- './ee/spec/services/audit_events/protected_branch_audit_event_service_spec.rb'
- './ee/spec/services/audit_events/register_runner_audit_event_service_spec.rb'
- './ee/spec/services/audit_events/release_artifacts_downloaded_audit_event_service_spec.rb'
@@ -2700,41 +2634,20 @@
- './ee/spec/services/geo/cache_invalidation_event_store_spec.rb'
- './ee/spec/services/geo/container_repository_sync_service_spec.rb'
- './ee/spec/services/geo/container_repository_sync_spec.rb'
-- './ee/spec/services/geo/design_repository_sync_service_spec.rb'
- './ee/spec/services/geo/event_service_spec.rb'
- './ee/spec/services/geo/file_registry_removal_service_spec.rb'
-- './ee/spec/services/geo/files_expire_service_spec.rb'
- './ee/spec/services/geo/framework_repository_sync_service_spec.rb'
- './ee/spec/services/geo/graphql_request_service_spec.rb'
- './ee/spec/services/geo/hashed_storage_attachments_event_store_spec.rb'
- './ee/spec/services/geo/hashed_storage_attachments_migration_service_spec.rb'
-- './ee/spec/services/geo/hashed_storage_migrated_event_store_spec.rb'
-- './ee/spec/services/geo/hashed_storage_migration_service_spec.rb'
- './ee/spec/services/geo/metrics_update_service_spec.rb'
-- './ee/spec/services/geo/move_repository_service_spec.rb'
- './ee/spec/services/geo/node_create_service_spec.rb'
- './ee/spec/services/geo/node_status_request_service_spec.rb'
- './ee/spec/services/geo/node_update_service_spec.rb'
-- './ee/spec/services/geo/project_housekeeping_service_spec.rb'
- './ee/spec/services/geo/prune_event_log_service_spec.rb'
- './ee/spec/services/geo/registry_consistency_service_spec.rb'
-- './ee/spec/services/geo/rename_repository_service_spec.rb'
- './ee/spec/services/geo/replication_toggle_request_service_spec.rb'
-- './ee/spec/services/geo/repositories_changed_event_store_spec.rb'
-- './ee/spec/services/geo/repository_base_sync_service_spec.rb'
-- './ee/spec/services/geo/repository_created_event_store_spec.rb'
-- './ee/spec/services/geo/repository_deleted_event_store_spec.rb'
-- './ee/spec/services/geo/repository_destroy_service_spec.rb'
- './ee/spec/services/geo/repository_registry_removal_service_spec.rb'
-- './ee/spec/services/geo/repository_renamed_event_store_spec.rb'
-- './ee/spec/services/geo/repository_sync_service_spec.rb'
-- './ee/spec/services/geo/repository_updated_event_store_spec.rb'
-- './ee/spec/services/geo/repository_updated_service_spec.rb'
-- './ee/spec/services/geo/repository_verification_primary_service_spec.rb'
-- './ee/spec/services/geo/repository_verification_reset_spec.rb'
-- './ee/spec/services/geo/repository_verification_secondary_service_spec.rb'
-- './ee/spec/services/geo/reset_checksum_event_store_spec.rb'
-- './ee/spec/services/geo/wiki_sync_service_spec.rb'
- './ee/spec/services/gitlab_subscriptions/activate_service_spec.rb'
- './ee/spec/services/gitlab_subscriptions/check_future_renewal_service_spec.rb'
- './ee/spec/services/gitlab_subscriptions/create_service_spec.rb'
@@ -2823,7 +2736,6 @@
- './ee/spec/services/personal_access_tokens/revoke_invalid_tokens_spec.rb'
- './ee/spec/services/personal_access_tokens/revoke_service_audit_log_spec.rb'
- './ee/spec/services/personal_access_tokens/rotation_verifier_service_spec.rb'
-- './ee/spec/services/projects/after_rename_service_spec.rb'
- './ee/spec/services/projects/alerting/notify_service_spec.rb'
- './ee/spec/services/projects/cleanup_service_spec.rb'
- './ee/spec/services/projects/create_from_template_service_spec.rb'
@@ -2838,13 +2750,11 @@
- './ee/spec/services/projects/group_links/destroy_service_spec.rb'
- './ee/spec/services/projects/group_links/update_service_spec.rb'
- './ee/spec/services/projects/hashed_storage/migrate_attachments_service_spec.rb'
-- './ee/spec/services/projects/hashed_storage/migrate_repository_service_spec.rb'
- './ee/spec/services/projects/import_export/export_service_spec.rb'
- './ee/spec/services/projects/import_service_spec.rb'
- './ee/spec/services/projects/mark_for_deletion_service_spec.rb'
- './ee/spec/services/projects/open_issues_count_service_spec.rb'
- './ee/spec/services/projects/operations/update_service_spec.rb'
-- './ee/spec/services/projects/prometheus/alerts/notify_service_spec.rb'
- './ee/spec/services/projects/protect_default_branch_service_spec.rb'
- './ee/spec/services/projects/restore_service_spec.rb'
- './ee/spec/services/projects/setup_ci_cd_spec.rb'
@@ -2894,7 +2804,6 @@
- './ee/spec/services/security/ingestion/tasks/ingest_finding_signatures_spec.rb'
- './ee/spec/services/security/ingestion/tasks/ingest_findings_spec.rb'
- './ee/spec/services/security/ingestion/tasks/ingest_identifiers_spec.rb'
-- './ee/spec/services/security/ingestion/tasks/ingest_issue_links_spec.rb'
- './ee/spec/services/security/ingestion/tasks/ingest_remediations_spec.rb'
- './ee/spec/services/security/ingestion/tasks/ingest_vulnerabilities/create_spec.rb'
- './ee/spec/services/security/ingestion/tasks/ingest_vulnerabilities/mark_resolved_as_detected_spec.rb'
@@ -2925,7 +2834,6 @@
- './ee/spec/services/security/security_orchestration_policies/rule_schedule_service_spec.rb'
- './ee/spec/services/security/security_orchestration_policies/scan_pipeline_service_spec.rb'
- './ee/spec/services/security/security_orchestration_policies/sync_opened_merge_requests_service_spec.rb'
-- './ee/spec/services/security/security_orchestration_policies/sync_open_merge_requests_head_pipeline_service_spec.rb'
- './ee/spec/services/security/security_orchestration_policies/validate_policy_service_spec.rb'
- './ee/spec/services/security/store_grouped_scans_service_spec.rb'
- './ee/spec/services/security/store_scan_service_spec.rb'
@@ -2953,14 +2861,12 @@
- './ee/spec/services/users/abuse/git_abuse/namespace_throttle_service_spec.rb'
- './ee/spec/services/users/abuse/namespace_bans/create_service_spec.rb'
- './ee/spec/services/users/abuse/namespace_bans/destroy_service_spec.rb'
-- './ee/spec/services/users/captcha_challenge_service_spec.rb'
- './ee/spec/services/users_ops_dashboard_projects/destroy_service_spec.rb'
- './ee/spec/services/users/update_highest_member_role_service_spec.rb'
- './ee/spec/services/vulnerabilities/confirm_service_spec.rb'
- './ee/spec/services/vulnerabilities/create_service_spec.rb'
- './ee/spec/services/vulnerabilities/destroy_dismissal_feedback_service_spec.rb'
- './ee/spec/services/vulnerabilities/dismiss_service_spec.rb'
-- './ee/spec/services/vulnerabilities/finding_dismiss_service_spec.rb'
- './ee/spec/services/vulnerabilities/historical_statistics/adjustment_service_spec.rb'
- './ee/spec/services/vulnerabilities/historical_statistics/deletion_service_spec.rb'
- './ee/spec/services/vulnerabilities/manually_create_service_spec.rb'
@@ -2990,7 +2896,6 @@
- './ee/spec/services/wikis/create_attachment_service_spec.rb'
- './ee/spec/services/work_items/update_service_spec.rb'
- './ee/spec/services/work_items/widgets/weight_service/update_service_spec.rb'
-- './ee/spec/tasks/geo/git_rake_spec.rb'
- './ee/spec/tasks/geo_rake_spec.rb'
- './ee/spec/tasks/gitlab/check_rake_spec.rb'
- './ee/spec/tasks/gitlab/elastic_rake_spec.rb'
@@ -3019,9 +2924,7 @@
- './ee/spec/views/compliance_management/compliance_framework/_project_settings.html.haml_spec.rb'
- './ee/spec/views/devise/sessions/new.html.haml_spec.rb'
- './ee/spec/views/groups/billings/index.html.haml_spec.rb'
-- './ee/spec/views/groups/compliance_frameworks/edit.html.haml_spec.rb'
- './ee/spec/views/groups/_compliance_frameworks.html.haml_spec.rb'
-- './ee/spec/views/groups/compliance_frameworks/new.html.haml_spec.rb'
- './ee/spec/views/groups/edit.html.haml_spec.rb'
- './ee/spec/views/groups/hook_logs/show.html.haml_spec.rb'
- './ee/spec/views/groups/hooks/edit.html.haml_spec.rb'
@@ -3041,7 +2944,6 @@
- './ee/spec/views/operations/index.html.haml_spec.rb'
- './ee/spec/views/profiles/preferences/show.html.haml_spec.rb'
- './ee/spec/views/projects/edit.html.haml_spec.rb'
-- './ee/spec/views/projects/issues/show.html.haml_spec.rb'
- './ee/spec/views/projects/on_demand_scans/index.html.haml_spec.rb'
- './ee/spec/views/projects/security/corpus_management/show.html.haml_spec.rb'
- './ee/spec/views/projects/security/dast_profiles/show.html.haml_spec.rb'
@@ -3137,19 +3039,13 @@
- './ee/spec/workers/elastic_remove_expired_namespace_subscriptions_from_index_cron_worker_spec.rb'
- './ee/spec/workers/epics/new_epic_issue_worker_spec.rb'
- './ee/spec/workers/geo/batch_event_create_worker_spec.rb'
-- './ee/spec/workers/geo/batch/project_registry_scheduler_worker_spec.rb'
-- './ee/spec/workers/geo/batch/project_registry_worker_spec.rb'
- './ee/spec/workers/geo/container_repository_sync_worker_spec.rb'
- './ee/spec/workers/geo/create_repository_updated_event_worker_spec.rb'
-- './ee/spec/workers/geo/design_repository_shard_sync_worker_spec.rb'
-- './ee/spec/workers/geo/design_repository_sync_worker_spec.rb'
- './ee/spec/workers/geo/destroy_worker_spec.rb'
- './ee/spec/workers/geo/event_worker_spec.rb'
- './ee/spec/workers/geo/metrics_update_worker_spec.rb'
-- './ee/spec/workers/geo/project_sync_worker_spec.rb'
- './ee/spec/workers/geo/prune_event_log_worker_spec.rb'
- './ee/spec/workers/geo/registry_sync_worker_spec.rb'
-- './ee/spec/workers/geo/repositories_clean_up_worker_spec.rb'
- './ee/spec/workers/geo/reverification_batch_worker_spec.rb'
- './ee/spec/workers/geo/scheduler/scheduler_worker_spec.rb'
- './ee/spec/workers/geo/secondary/registry_consistency_worker_spec.rb'
@@ -3190,7 +3086,6 @@
- './ee/spec/workers/project_import_schedule_worker_spec.rb'
- './ee/spec/workers/projects/disable_legacy_open_source_license_for_inactive_projects_worker_spec.rb'
- './ee/spec/workers/project_template_export_worker_spec.rb'
-- './ee/spec/workers/refresh_license_compliance_checks_worker_spec.rb'
- './ee/spec/workers/repository_import_worker_spec.rb'
- './ee/spec/workers/repository_update_mirror_worker_spec.rb'
- './ee/spec/workers/requirements_management/import_requirements_csv_worker_spec.rb'
@@ -3286,12 +3181,10 @@
- './spec/controllers/concerns/internal_redirect_spec.rb'
- './spec/controllers/concerns/issuable_actions_spec.rb'
- './spec/controllers/concerns/issuable_collections_spec.rb'
-- './spec/controllers/concerns/metrics_dashboard_spec.rb'
- './spec/controllers/concerns/page_limiter_spec.rb'
- './spec/controllers/concerns/product_analytics_tracking_spec.rb'
- './spec/controllers/concerns/project_unauthorized_spec.rb'
- './spec/controllers/concerns/redirects_for_missing_path_on_tree_spec.rb'
-- './spec/controllers/concerns/redis_tracking_spec.rb'
- './spec/controllers/concerns/renders_commits_spec.rb'
- './spec/controllers/concerns/routable_actions_spec.rb'
- './spec/controllers/concerns/send_file_upload_spec.rb'
@@ -3347,7 +3240,6 @@
- './spec/controllers/import/fogbugz_controller_spec.rb'
- './spec/controllers/import/gitea_controller_spec.rb'
- './spec/controllers/import/github_controller_spec.rb'
-- './spec/controllers/import/gitlab_controller_spec.rb'
- './spec/controllers/import/manifest_controller_spec.rb'
- './spec/controllers/invites_controller_spec.rb'
- './spec/controllers/jira_connect/app_descriptor_controller_spec.rb'
@@ -3359,7 +3251,6 @@
- './spec/controllers/oauth/applications_controller_spec.rb'
- './spec/controllers/oauth/authorizations_controller_spec.rb'
- './spec/controllers/oauth/authorized_applications_controller_spec.rb'
-- './spec/controllers/oauth/jira_dvcs/authorizations_controller_spec.rb'
- './spec/controllers/oauth/token_info_controller_spec.rb'
- './spec/controllers/oauth/tokens_controller_spec.rb'
- './spec/controllers/omniauth_callbacks_controller_spec.rb'
@@ -3377,7 +3268,6 @@
- './spec/controllers/profiles/two_factor_auths_controller_spec.rb'
- './spec/controllers/profiles/webauthn_registrations_controller_spec.rb'
- './spec/controllers/projects/alerting/notifications_controller_spec.rb'
-- './spec/controllers/projects/alert_management_controller_spec.rb'
- './spec/controllers/projects/analytics/cycle_analytics/stages_controller_spec.rb'
- './spec/controllers/projects/analytics/cycle_analytics/summary_controller_spec.rb'
- './spec/controllers/projects/analytics/cycle_analytics/value_streams_controller_spec.rb'
@@ -3405,8 +3295,6 @@
- './spec/controllers/projects/design_management/designs/resized_image_controller_spec.rb'
- './spec/controllers/projects/discussions_controller_spec.rb'
- './spec/controllers/projects/environments_controller_spec.rb'
-- './spec/controllers/projects/environments/prometheus_api_controller_spec.rb'
-- './spec/controllers/projects/environments/sample_metrics_controller_spec.rb'
- './spec/controllers/projects/error_tracking_controller_spec.rb'
- './spec/controllers/projects/error_tracking/projects_controller_spec.rb'
- './spec/controllers/projects/error_tracking/stack_traces_controller_spec.rb'
@@ -3415,7 +3303,6 @@
- './spec/controllers/projects/feature_flags_user_lists_controller_spec.rb'
- './spec/controllers/projects/find_file_controller_spec.rb'
- './spec/controllers/projects/forks_controller_spec.rb'
-- './spec/controllers/projects/grafana_api_controller_spec.rb'
- './spec/controllers/projects/graphs_controller_spec.rb'
- './spec/controllers/projects/group_links_controller_spec.rb'
- './spec/controllers/projects/hooks_controller_spec.rb'
@@ -3439,15 +3326,12 @@
- './spec/controllers/projects/packages/packages_controller_spec.rb'
- './spec/controllers/projects/pages_controller_spec.rb'
- './spec/controllers/projects/pages_domains_controller_spec.rb'
-- './spec/controllers/projects/performance_monitoring/dashboards_controller_spec.rb'
- './spec/controllers/projects/pipeline_schedules_controller_spec.rb'
- './spec/controllers/projects/pipelines_controller_spec.rb'
- './spec/controllers/projects/pipelines_settings_controller_spec.rb'
- './spec/controllers/projects/pipelines/stages_controller_spec.rb'
- './spec/controllers/projects/pipelines/tests_controller_spec.rb'
- './spec/controllers/projects/project_members_controller_spec.rb'
-- './spec/controllers/projects/prometheus/alerts_controller_spec.rb'
-- './spec/controllers/projects/prometheus/metrics_controller_spec.rb'
- './spec/controllers/projects/protected_branches_controller_spec.rb'
- './spec/controllers/projects/protected_tags_controller_spec.rb'
- './spec/controllers/projects/raw_controller_spec.rb'
@@ -3458,7 +3342,6 @@
- './spec/controllers/projects/releases/evidences_controller_spec.rb'
- './spec/controllers/projects/repositories_controller_spec.rb'
- './spec/controllers/projects/security/configuration_controller_spec.rb'
-- './spec/controllers/projects/service_desk_controller_spec.rb'
- './spec/controllers/projects/service_ping_controller_spec.rb'
- './spec/controllers/projects/settings/ci_cd_controller_spec.rb'
- './spec/controllers/projects/settings/integration_hook_logs_controller_spec.rb'
@@ -3494,17 +3377,14 @@
- './spec/controllers/users/terms_controller_spec.rb'
- './spec/controllers/users/unsubscribes_controller_spec.rb'
- './spec/db/development/create_base_work_item_types_spec.rb'
-- './spec/db/development/import_common_metrics_spec.rb'
- './spec/db/docs_spec.rb'
- './spec/db/migration_spec.rb'
- './spec/db/production/create_base_work_item_types_spec.rb'
-- './spec/db/production/import_common_metrics_spec.rb'
- './spec/db/production/settings_spec.rb'
- './spec/db/schema_spec.rb'
- './spec/dependencies/omniauth_saml_spec.rb'
- './spec/experiments/application_experiment_spec.rb'
- './spec/experiments/in_product_guidance_environments_webide_experiment_spec.rb'
-- './spec/experiments/ios_specific_templates_experiment_spec.rb'
- './spec/features/abuse_report_spec.rb'
- './spec/features/action_cable_logging_spec.rb'
- './spec/features/admin/admin_abuse_reports_spec.rb'
@@ -3575,7 +3455,6 @@
- './spec/features/callouts/registration_enabled_spec.rb'
- './spec/features/canonical_link_spec.rb'
- './spec/features/clusters/cluster_detail_page_spec.rb'
-- './spec/features/clusters/cluster_health_dashboard_spec.rb'
- './spec/features/clusters/create_agent_spec.rb'
- './spec/features/commit_spec.rb'
- './spec/features/commits_spec.rb'
@@ -3592,7 +3471,6 @@
- './spec/features/dashboard/issuables_counter_spec.rb'
- './spec/features/dashboard/issues_filter_spec.rb'
- './spec/features/dashboard/issues_spec.rb'
-- './spec/features/dashboard/label_filter_spec.rb'
- './spec/features/dashboard/merge_requests_spec.rb'
- './spec/features/dashboard/milestones_spec.rb'
- './spec/features/dashboard/project_member_activity_index_spec.rb'
@@ -3767,7 +3645,6 @@
- './spec/features/issues/user_views_issues_spec.rb'
- './spec/features/jira_connect/branches_spec.rb'
- './spec/features/jira_connect/subscriptions_spec.rb'
-- './spec/features/jira_oauth_provider_authorize_spec.rb'
- './spec/features/labels_hierarchy_spec.rb'
- './spec/features/markdown/copy_as_gfm_spec.rb'
- './spec/features/markdown/gitlab_flavored_markdown_spec.rb'
@@ -3776,7 +3653,6 @@
- './spec/features/markdown/kroki_spec.rb'
- './spec/features/markdown/markdown_spec.rb'
- './spec/features/markdown/math_spec.rb'
-- './spec/features/markdown/metrics_spec.rb'
- './spec/features/markdown/sandboxed_mermaid_spec.rb'
- './spec/features/merge_request/batch_comments_spec.rb'
- './spec/features/merge_request/close_reopen_report_toggle_spec.rb'
@@ -3827,7 +3703,6 @@
- './spec/features/merge_request/user_merges_immediately_spec.rb'
- './spec/features/merge_request/user_merges_merge_request_spec.rb'
- './spec/features/merge_request/user_merges_only_if_pipeline_succeeds_spec.rb'
-- './spec/features/merge_request/user_merges_when_pipeline_succeeds_spec.rb'
- './spec/features/merge_request/user_opens_checkout_branch_modal_spec.rb'
- './spec/features/merge_request/user_opens_context_commits_modal_spec.rb'
- './spec/features/merge_request/user_posts_diff_notes_spec.rb'
@@ -3883,11 +3758,8 @@
- './spec/features/milestones/user_views_milestone_spec.rb'
- './spec/features/milestones/user_views_milestones_spec.rb'
- './spec/features/monitor_sidebar_link_spec.rb'
-- './spec/features/nav/top_nav_responsive_spec.rb'
-- './spec/features/nav/top_nav_tooltip_spec.rb'
- './spec/features/oauth_login_spec.rb'
- './spec/features/oauth_provider_authorize_spec.rb'
-- './spec/features/oauth_registration_spec.rb'
- './spec/features/one_trust_spec.rb'
- './spec/features/participants_autocomplete_spec.rb'
- './spec/features/password_reset_spec.rb'
@@ -3905,16 +3777,13 @@
- './spec/features/profiles/two_factor_auths_spec.rb'
- './spec/features/profiles/user_changes_notified_of_own_activity_spec.rb'
- './spec/features/profiles/user_edit_preferences_spec.rb'
-- './spec/features/profiles/user_edit_profile_spec.rb'
- './spec/features/profiles/user_manages_applications_spec.rb'
- './spec/features/profiles/user_manages_emails_spec.rb'
- './spec/features/profiles/user_search_settings_spec.rb'
- './spec/features/profiles/user_visits_notifications_tab_spec.rb'
-- './spec/features/profiles/user_visits_profile_account_page_spec.rb'
- './spec/features/profiles/user_visits_profile_authentication_log_spec.rb'
- './spec/features/profiles/user_visits_profile_preferences_page_spec.rb'
- './spec/features/profiles/user_visits_profile_spec.rb'
-- './spec/features/profiles/user_visits_profile_ssh_keys_page_spec.rb'
- './spec/features/project_group_variables_spec.rb'
- './spec/features/projects/active_tabs_spec.rb'
- './spec/features/projects/activity/rss_spec.rb'
@@ -3967,7 +3836,6 @@
- './spec/features/projects/container_registry_spec.rb'
- './spec/features/projects/deploy_keys_spec.rb'
- './spec/features/projects/diffs/diff_show_spec.rb'
-- './spec/features/projects/environments/environment_metrics_spec.rb'
- './spec/features/projects/environments/environment_spec.rb'
- './spec/features/projects/environments/environments_spec.rb'
- './spec/features/projects/feature_flags/user_creates_feature_flag_spec.rb'
@@ -4023,7 +3891,6 @@
- './spec/features/projects/integrations/user_activates_mattermost_slash_command_spec.rb'
- './spec/features/projects/integrations/user_activates_packagist_spec.rb'
- './spec/features/projects/integrations/user_activates_pivotaltracker_spec.rb'
-- './spec/features/projects/integrations/user_activates_prometheus_spec.rb'
- './spec/features/projects/integrations/user_activates_pushover_spec.rb'
- './spec/features/projects/integrations/user_activates_slack_notifications_spec.rb'
- './spec/features/projects/integrations/user_activates_slack_slash_command_spec.rb'
@@ -4143,7 +4010,6 @@
- './spec/features/projects/show/user_sees_setup_shortcut_buttons_spec.rb'
- './spec/features/projects/show/user_uploads_files_spec.rb'
- './spec/features/projects/snippets/create_snippet_spec.rb'
-- './spec/features/projects/snippets/show_spec.rb'
- './spec/features/projects/snippets/user_comments_on_snippet_spec.rb'
- './spec/features/projects/snippets/user_deletes_snippet_spec.rb'
- './spec/features/projects/snippets/user_updates_snippet_spec.rb'
@@ -4214,7 +4080,6 @@
- './spec/features/snippets/private_snippets_spec.rb'
- './spec/features/snippets/public_snippets_spec.rb'
- './spec/features/snippets/search_snippets_spec.rb'
-- './spec/features/snippets/show_spec.rb'
- './spec/features/snippets/spam_snippets_spec.rb'
- './spec/features/snippets_spec.rb'
- './spec/features/snippets/user_creates_snippet_spec.rb'
@@ -4237,12 +4102,10 @@
- './spec/features/user_opens_link_to_comment_spec.rb'
- './spec/features/users/active_sessions_spec.rb'
- './spec/features/users/add_email_to_existing_account_spec.rb'
-- './spec/features/users/anonymous_sessions_spec.rb'
- './spec/features/users/bizible_csp_spec.rb'
- './spec/features/users/confirmation_spec.rb'
- './spec/features/user_sees_revert_modal_spec.rb'
- './spec/features/users/email_verification_on_login_spec.rb'
-- './spec/features/users/google_analytics_csp_spec.rb'
- './spec/features/users/login_spec.rb'
- './spec/features/users/logout_spec.rb'
- './spec/features/users/one_trust_csp_spec.rb'
@@ -4338,7 +4201,6 @@
- './spec/finders/merge_requests_finder_spec.rb'
- './spec/finders/merge_requests/oldest_per_commit_finder_spec.rb'
- './spec/finders/merge_request_target_project_finder_spec.rb'
-- './spec/finders/metrics/users_starred_dashboards_finder_spec.rb'
- './spec/finders/milestones_finder_spec.rb'
- './spec/finders/namespaces/projects_finder_spec.rb'
- './spec/finders/notes_finder_spec.rb'
@@ -4403,7 +4265,6 @@
- './spec/finders/users_finder_spec.rb'
- './spec/finders/users_star_projects_finder_spec.rb'
- './spec/finders/work_items/work_items_finder_spec.rb'
-- './spec/frontend/fixtures/abuse_reports.rb'
- './spec/frontend/fixtures/admin_users.rb'
- './spec/frontend/fixtures/analytics.rb'
- './spec/frontend/fixtures/api_deploy_keys.rb'
@@ -4426,7 +4287,6 @@
- './spec/frontend/fixtures/listbox.rb'
- './spec/frontend/fixtures/merge_requests_diffs.rb'
- './spec/frontend/fixtures/merge_requests.rb'
-- './spec/frontend/fixtures/metrics_dashboard.rb'
- './spec/frontend/fixtures/namespaces.rb'
- './spec/frontend/fixtures/pipeline_schedules.rb'
- './spec/frontend/fixtures/pipelines.rb'
@@ -4439,7 +4299,6 @@
- './spec/frontend/fixtures/search.rb'
- './spec/frontend/fixtures/sessions.rb'
- './spec/frontend/fixtures/snippet.rb'
-- './spec/frontend/fixtures/startup_css.rb'
- './spec/frontend/fixtures/tabs.rb'
- './spec/frontend/fixtures/tags.rb'
- './spec/frontend/fixtures/timezones.rb'
@@ -4459,7 +4318,6 @@
- './spec/graphql/mutations/alert_management/prometheus_integration/reset_token_spec.rb'
- './spec/graphql/mutations/alert_management/prometheus_integration/update_spec.rb'
- './spec/graphql/mutations/alert_management/update_alert_status_spec.rb'
-- './spec/graphql/mutations/base_mutation_spec.rb'
- './spec/graphql/mutations/boards/issues/issue_move_list_spec.rb'
- './spec/graphql/mutations/boards/lists/create_spec.rb'
- './spec/graphql/mutations/boards/lists/update_spec.rb'
@@ -4614,7 +4472,6 @@
- './spec/graphql/resolvers/merge_requests_count_resolver_spec.rb'
- './spec/graphql/resolvers/merge_requests_resolver_spec.rb'
- './spec/graphql/resolvers/metadata_resolver_spec.rb'
-- './spec/graphql/resolvers/metrics/dashboard_resolver_spec.rb'
- './spec/graphql/resolvers/metrics/dashboards/annotation_resolver_spec.rb'
- './spec/graphql/resolvers/namespace_projects_resolver_spec.rb'
- './spec/graphql/resolvers/package_details_resolver_spec.rb'
@@ -4830,7 +4687,6 @@
- './spec/graphql/types/metadata/kas_type_spec.rb'
- './spec/graphql/types/metadata_type_spec.rb'
- './spec/graphql/types/metrics/dashboards/annotation_type_spec.rb'
-- './spec/graphql/types/metrics/dashboard_type_spec.rb'
- './spec/graphql/types/milestone_stats_type_spec.rb'
- './spec/graphql/types/milestone_type_spec.rb'
- './spec/graphql/types/mutation_type_spec.rb'
@@ -5051,7 +4907,6 @@
- './spec/helpers/search_helper_spec.rb'
- './spec/helpers/sessions_helper_spec.rb'
- './spec/helpers/sidebars_helper_spec.rb'
-- './spec/helpers/sidekiq_helper_spec.rb'
- './spec/helpers/snippets_helper_spec.rb'
- './spec/helpers/sorting_helper_spec.rb'
- './spec/helpers/sourcegraph_helper_spec.rb'
@@ -5082,7 +4937,6 @@
- './spec/helpers/wiki_helper_spec.rb'
- './spec/helpers/wiki_page_version_helper_spec.rb'
- './spec/helpers/x509_helper_spec.rb'
-- './spec/initializers/00_rails_disable_joins_spec.rb'
- './spec/initializers/0_postgresql_types_spec.rb'
- './spec/initializers/100_patch_omniauth_oauth2_spec.rb'
- './spec/initializers/100_patch_omniauth_saml_spec.rb'
@@ -5092,7 +4946,6 @@
- './spec/initializers/action_mailer_hooks_spec.rb'
- './spec/initializers/active_record_locking_spec.rb'
- './spec/initializers/asset_proxy_setting_spec.rb'
-- './spec/initializers/carrierwave_patch_spec.rb'
- './spec/initializers/cookies_serializer_spec.rb'
- './spec/initializers/database_config_spec.rb'
- './spec/initializers/diagnostic_reports_spec.rb'
@@ -5103,7 +4956,6 @@
- './spec/initializers/forbid_sidekiq_in_transactions_spec.rb'
- './spec/initializers/global_id_spec.rb'
- './spec/initializers/google_api_client_spec.rb'
-- './spec/initializers/hangouts_chat_http_override_spec.rb'
- './spec/initializers/lograge_spec.rb'
- './spec/initializers/mail_encoding_patch_spec.rb'
- './spec/initializers/mailer_retries_spec.rb'
@@ -5172,7 +5024,6 @@
- './spec/lib/api/entities/user_spec.rb'
- './spec/lib/api/entities/wiki_page_spec.rb'
- './spec/lib/api/every_api_endpoint_spec.rb'
-- './spec/lib/api/github/entities_spec.rb'
- './spec/lib/api/helpers/authentication_spec.rb'
- './spec/lib/api/helpers/caching_spec.rb'
- './spec/lib/api/helpers/common_helpers_spec.rb'
@@ -5246,12 +5097,7 @@
- './spec/lib/banzai/filter/html_entity_filter_spec.rb'
- './spec/lib/banzai/filter/image_lazy_load_filter_spec.rb'
- './spec/lib/banzai/filter/image_link_filter_spec.rb'
-- './spec/lib/banzai/filter/inline_alert_metrics_filter_spec.rb'
-- './spec/lib/banzai/filter/inline_cluster_metrics_filter_spec.rb'
- './spec/lib/banzai/filter/inline_diff_filter_spec.rb'
-- './spec/lib/banzai/filter/inline_grafana_metrics_filter_spec.rb'
-- './spec/lib/banzai/filter/inline_metrics_filter_spec.rb'
-- './spec/lib/banzai/filter/inline_metrics_redactor_filter_spec.rb'
- './spec/lib/banzai/filter/issuable_reference_expansion_filter_spec.rb'
- './spec/lib/banzai/filter/jira_import/adf_to_commonmark_filter_spec.rb'
- './spec/lib/banzai/filter/kroki_filter_spec.rb'
@@ -5363,7 +5209,6 @@
- './spec/lib/bulk_imports/common/pipelines/wiki_pipeline_spec.rb'
- './spec/lib/bulk_imports/common/rest/get_badges_query_spec.rb'
- './spec/lib/bulk_imports/common/transformers/prohibited_attributes_transformer_spec.rb'
-- './spec/lib/bulk_imports/common/transformers/user_reference_transformer_spec.rb'
- './spec/lib/bulk_imports/groups/extractors/subgroups_extractor_spec.rb'
- './spec/lib/bulk_imports/groups/graphql/get_group_query_spec.rb'
- './spec/lib/bulk_imports/groups/graphql/get_projects_query_spec.rb'
@@ -5430,7 +5275,6 @@
- './spec/lib/feature_spec.rb'
- './spec/lib/file_size_validator_spec.rb'
- './spec/lib/forever_spec.rb'
-- './spec/lib/generators/gitlab/snowplow_event_definition_generator_spec.rb'
- './spec/lib/generators/gitlab/usage_metric_definition_generator_spec.rb'
- './spec/lib/generators/gitlab/usage_metric_definition/redis_hll_generator_spec.rb'
- './spec/lib/generators/gitlab/usage_metric_generator_spec.rb'
@@ -5442,7 +5286,6 @@
- './spec/lib/gitlab/alert_management/fingerprint_spec.rb'
- './spec/lib/gitlab/alert_management/payload/base_spec.rb'
- './spec/lib/gitlab/alert_management/payload/generic_spec.rb'
-- './spec/lib/gitlab/alert_management/payload/managed_prometheus_spec.rb'
- './spec/lib/gitlab/alert_management/payload/prometheus_spec.rb'
- './spec/lib/gitlab/alert_management/payload_spec.rb'
- './spec/lib/gitlab/allowable_spec.rb'
@@ -5531,14 +5374,8 @@
- './spec/lib/gitlab/auth/unique_ips_limiter_spec.rb'
- './spec/lib/gitlab/auth/user_access_denied_reason_spec.rb'
- './spec/lib/gitlab/avatar_cache_spec.rb'
-- './spec/lib/gitlab/background_migration/backfill_draft_status_on_merge_requests_spec.rb'
-- './spec/lib/gitlab/background_migration/backfill_draft_status_on_merge_requests_with_corrected_regex_spec.rb'
-- './spec/lib/gitlab/background_migration/backfill_group_features_spec.rb'
- './spec/lib/gitlab/background_migration/backfill_imported_issue_search_data_spec.rb'
- './spec/lib/gitlab/background_migration/backfill_integrations_enable_ssl_verification_spec.rb'
-- './spec/lib/gitlab/background_migration/backfill_issue_search_data_spec.rb'
-- './spec/lib/gitlab/background_migration/backfill_member_namespace_for_group_members_spec.rb'
-- './spec/lib/gitlab/background_migration/backfill_namespace_id_for_namespace_route_spec.rb'
- './spec/lib/gitlab/background_migration/backfill_namespace_id_for_project_route_spec.rb'
- './spec/lib/gitlab/background_migration/backfill_namespace_id_of_vulnerability_reads_spec.rb'
- './spec/lib/gitlab/background_migration/backfill_note_discussion_id_spec.rb'
@@ -5546,20 +5383,17 @@
- './spec/lib/gitlab/background_migration/backfill_project_import_level_spec.rb'
- './spec/lib/gitlab/background_migration/backfill_project_member_namespace_id_spec.rb'
- './spec/lib/gitlab/background_migration/backfill_project_repositories_spec.rb'
-- './spec/lib/gitlab/background_migration/backfill_project_settings_spec.rb'
- './spec/lib/gitlab/background_migration/backfill_snippet_repositories_spec.rb'
- './spec/lib/gitlab/background_migration/backfill_topics_title_spec.rb'
- './spec/lib/gitlab/background_migration/backfill_vulnerability_reads_cluster_agent_spec.rb'
- './spec/lib/gitlab/background_migration/backfill_work_item_type_id_for_issues_spec.rb'
- './spec/lib/gitlab/background_migration/base_job_spec.rb'
- './spec/lib/gitlab/background_migration/batched_migration_job_spec.rb'
-- './spec/lib/gitlab/background_migration/batching_strategies/backfill_project_namespace_per_group_batching_strategy_spec.rb'
- './spec/lib/gitlab/background_migration/batching_strategies/backfill_project_statistics_with_container_registry_size_batching_strategy_spec.rb'
- './spec/lib/gitlab/background_migration/batching_strategies/base_strategy_spec.rb'
- './spec/lib/gitlab/background_migration/batching_strategies/dismissed_vulnerabilities_strategy_spec.rb'
- './spec/lib/gitlab/background_migration/batching_strategies/loose_index_scan_batching_strategy_spec.rb'
- './spec/lib/gitlab/background_migration/batching_strategies/primary_key_batching_strategy_spec.rb'
-- './spec/lib/gitlab/background_migration/cleanup_draft_data_from_faulty_regex_spec.rb'
- './spec/lib/gitlab/background_migration/cleanup_orphaned_routes_spec.rb'
- './spec/lib/gitlab/background_migration/copy_column_using_background_migration_job_spec.rb'
- './spec/lib/gitlab/background_migration/destroy_invalid_group_members_spec.rb'
@@ -5568,23 +5402,12 @@
- './spec/lib/gitlab/background_migration/disable_legacy_open_source_license_for_no_issues_no_repo_projects_spec.rb'
- './spec/lib/gitlab/background_migration/disable_legacy_open_source_license_for_one_member_no_repo_projects_spec.rb'
- './spec/lib/gitlab/background_migration/expire_o_auth_tokens_spec.rb'
-- './spec/lib/gitlab/background_migration/fix_duplicate_project_name_and_path_spec.rb'
-- './spec/lib/gitlab/background_migration/fix_vulnerability_occurrences_with_hashes_as_raw_metadata_spec.rb'
- './spec/lib/gitlab/background_migration/job_coordinator_spec.rb'
- './spec/lib/gitlab/background_migration/legacy_upload_mover_spec.rb'
- './spec/lib/gitlab/background_migration/legacy_uploads_migrator_spec.rb'
- './spec/lib/gitlab/background_migration/mailers/unconfirm_mailer_spec.rb'
-- './spec/lib/gitlab/background_migration/migrate_personal_namespace_project_maintainer_to_owner_spec.rb'
-- './spec/lib/gitlab/background_migration/migrate_shimo_confluence_integration_category_spec.rb'
-- './spec/lib/gitlab/background_migration/nullify_orphan_runner_id_on_ci_builds_spec.rb'
-- './spec/lib/gitlab/background_migration/populate_container_repository_migration_plan_spec.rb'
- './spec/lib/gitlab/background_migration/populate_operation_visibility_permissions_from_operations_spec.rb'
-- './spec/lib/gitlab/background_migration/project_namespaces/backfill_project_namespaces_spec.rb'
-- './spec/lib/gitlab/background_migration/remove_occurrence_pipelines_and_duplicate_vulnerabilities_findings_spec.rb'
- './spec/lib/gitlab/background_migration/remove_self_managed_wiki_notes_spec.rb'
-- './spec/lib/gitlab/background_migration/remove_vulnerability_finding_links_spec.rb'
-- './spec/lib/gitlab/background_migration/reset_duplicate_ci_runners_token_encrypted_values_on_projects_spec.rb'
-- './spec/lib/gitlab/background_migration/reset_duplicate_ci_runners_token_values_on_projects_spec.rb'
- './spec/lib/gitlab/background_migration/reset_too_many_tags_skipped_registry_imports_spec.rb'
- './spec/lib/gitlab/background_migration/set_correct_vulnerability_state_spec.rb'
- './spec/lib/gitlab/background_migration/set_legacy_open_source_license_available_for_non_public_projects_spec.rb'
@@ -5597,7 +5420,6 @@
- './spec/lib/gitlab/bitbucket_import/importer_spec.rb'
- './spec/lib/gitlab/bitbucket_import/project_creator_spec.rb'
- './spec/lib/gitlab/bitbucket_import/wiki_formatter_spec.rb'
-- './spec/lib/gitlab/bitbucket_server_import/importer_spec.rb'
- './spec/lib/gitlab/blame_spec.rb'
- './spec/lib/gitlab/blob_helper_spec.rb'
- './spec/lib/gitlab/branch_push_merge_commit_analyzer_spec.rb'
@@ -5622,7 +5444,6 @@
- './spec/lib/gitlab/chat/responder/mattermost_spec.rb'
- './spec/lib/gitlab/chat/responder/slack_spec.rb'
- './spec/lib/gitlab/chat/responder_spec.rb'
-- './spec/lib/gitlab/chat_spec.rb'
- './spec/lib/gitlab/checks/branch_check_spec.rb'
- './spec/lib/gitlab/checks/changes_access_spec.rb'
- './spec/lib/gitlab/checks/container_moved_spec.rb'
@@ -5896,7 +5717,6 @@
- './spec/lib/gitlab/ci/status/build/skipped_spec.rb'
- './spec/lib/gitlab/ci/status/build/stop_spec.rb'
- './spec/lib/gitlab/ci/status/build/unschedule_spec.rb'
-- './spec/lib/gitlab/ci/status/build/waiting_for_approval_spec.rb'
- './spec/lib/gitlab/ci/status/build/waiting_for_resource_spec.rb'
- './spec/lib/gitlab/ci/status/canceled_spec.rb'
- './spec/lib/gitlab/ci/status/composite_spec.rb'
@@ -5984,7 +5804,6 @@
- './spec/lib/gitlab/composer/version_index_spec.rb'
- './spec/lib/gitlab/conan_token_spec.rb'
- './spec/lib/gitlab/config_checker/external_database_checker_spec.rb'
-- './spec/lib/gitlab/config_checker/puma_rugged_checker_spec.rb'
- './spec/lib/gitlab/config/entry/attributable_spec.rb'
- './spec/lib/gitlab/config/entry/boolean_spec.rb'
- './spec/lib/gitlab/config/entry/composable_array_spec.rb'
@@ -6029,9 +5848,6 @@
- './spec/lib/gitlab/database/background_migration/batched_migration_wrapper_spec.rb'
- './spec/lib/gitlab/database/background_migration/batch_metrics_spec.rb'
- './spec/lib/gitlab/database/background_migration/batch_optimizer_spec.rb'
-- './spec/lib/gitlab/database/background_migration/health_status/indicators/autovacuum_active_on_table_spec.rb'
-- './spec/lib/gitlab/database/background_migration/health_status/indicators/write_ahead_log_spec.rb'
-- './spec/lib/gitlab/database/background_migration/health_status_spec.rb'
- './spec/lib/gitlab/database/background_migration_job_spec.rb'
- './spec/lib/gitlab/database/background_migration/prometheus_metrics_spec.rb'
- './spec/lib/gitlab/database/batch_count_spec.rb'
@@ -6047,9 +5863,6 @@
- './spec/lib/gitlab/database/each_database_spec.rb'
- './spec/lib/gitlab/database/gitlab_schema_spec.rb'
- './spec/lib/gitlab/database/grant_spec.rb'
-- './spec/lib/gitlab/database_importers/common_metrics/importer_spec.rb'
-- './spec/lib/gitlab/database_importers/common_metrics/prometheus_metric_spec.rb'
-- './spec/lib/gitlab/database_importers/instance_administrators/create_group_spec.rb'
- './spec/lib/gitlab/database_importers/work_items/base_type_importer_spec.rb'
- './spec/lib/gitlab/database/load_balancing/action_cable_callbacks_spec.rb'
- './spec/lib/gitlab/database/load_balancing/configuration_spec.rb'
@@ -6116,7 +5929,6 @@
- './spec/lib/gitlab/database/postgres_partitioned_table_spec.rb'
- './spec/lib/gitlab/database/postgres_partition_spec.rb'
- './spec/lib/gitlab/database/postgresql_adapter/dump_schema_versions_mixin_spec.rb'
-- './spec/lib/gitlab/database/postgresql_adapter/empty_query_ping_spec.rb'
- './spec/lib/gitlab/database/postgresql_adapter/force_disconnectable_mixin_spec.rb'
- './spec/lib/gitlab/database/postgresql_adapter/type_map_cache_spec.rb'
- './spec/lib/gitlab/database/postgresql_database_tasks/load_schema_versions_mixin_spec.rb'
@@ -6132,10 +5944,6 @@
- './spec/lib/gitlab/database/reindexing/reindex_action_spec.rb'
- './spec/lib/gitlab/database/reindexing/reindex_concurrently_spec.rb'
- './spec/lib/gitlab/database/reindexing_spec.rb'
-- './spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_base_spec.rb'
-- './spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_namespaces_spec.rb'
-- './spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_projects_spec.rb'
-- './spec/lib/gitlab/database/rename_reserved_paths_migration/v1_spec.rb'
- './spec/lib/gitlab/database/schema_cache_with_renamed_table_spec.rb'
- './spec/lib/gitlab/database/schema_cleaner_spec.rb'
- './spec/lib/gitlab/database/schema_migrations/context_spec.rb'
@@ -6354,11 +6162,7 @@
- './spec/lib/gitlab/github_import/importer/note_importer_spec.rb'
- './spec/lib/gitlab/github_import/importer/notes_importer_spec.rb'
- './spec/lib/gitlab/github_import/importer/pull_request_importer_spec.rb'
-- './spec/lib/gitlab/github_import/importer/pull_request_merged_by_importer_spec.rb'
-- './spec/lib/gitlab/github_import/importer/pull_request_review_importer_spec.rb'
- './spec/lib/gitlab/github_import/importer/pull_requests_importer_spec.rb'
-- './spec/lib/gitlab/github_import/importer/pull_requests_merged_by_importer_spec.rb'
-- './spec/lib/gitlab/github_import/importer/pull_requests_reviews_importer_spec.rb'
- './spec/lib/gitlab/github_import/importer/releases_importer_spec.rb'
- './spec/lib/gitlab/github_import/importer/repository_importer_spec.rb'
- './spec/lib/gitlab/github_import/importer/single_endpoint_diff_notes_importer_spec.rb'
@@ -6406,7 +6210,6 @@
- './spec/lib/gitlab/git/remote_mirror_spec.rb'
- './spec/lib/gitlab/git/repository_cleaner_spec.rb'
- './spec/lib/gitlab/git/repository_spec.rb'
-- './spec/lib/gitlab/git/rugged_impl/use_rugged_spec.rb'
- './spec/lib/gitlab/git_spec.rb'
- './spec/lib/gitlab/git/tag_spec.rb'
- './spec/lib/gitlab/git/tree_spec.rb'
@@ -6471,7 +6274,6 @@
- './spec/lib/gitlab/harbor/client_spec.rb'
- './spec/lib/gitlab/harbor/query_spec.rb'
- './spec/lib/gitlab/hashed_path_spec.rb'
-- './spec/lib/gitlab/hashed_storage/migrator_spec.rb'
- './spec/lib/gitlab/health_checks/db_check_spec.rb'
- './spec/lib/gitlab/health_checks/gitaly_check_spec.rb'
- './spec/lib/gitlab/health_checks/master_check_spec.rb'
@@ -6535,7 +6337,6 @@
- './spec/lib/gitlab/import_export/json/ndjson_reader_spec.rb'
- './spec/lib/gitlab/import_export/json/ndjson_writer_spec.rb'
- './spec/lib/gitlab/import_export/json/streaming_serializer_spec.rb'
-- './spec/lib/gitlab/import_export/legacy_relation_tree_saver_spec.rb'
- './spec/lib/gitlab/import_export/lfs_restorer_spec.rb'
- './spec/lib/gitlab/import_export/lfs_saver_spec.rb'
- './spec/lib/gitlab/import_export/log_util_spec.rb'
@@ -6600,7 +6401,6 @@
- './spec/lib/gitlab/jira_import/labels_importer_spec.rb'
- './spec/lib/gitlab/jira_import/metadata_collector_spec.rb'
- './spec/lib/gitlab/jira_import_spec.rb'
-- './spec/lib/gitlab/jira/middleware_spec.rb'
- './spec/lib/gitlab/job_waiter_spec.rb'
- './spec/lib/gitlab/json_logger_spec.rb'
- './spec/lib/gitlab/json_spec.rb'
@@ -6685,27 +6485,6 @@
- './spec/lib/gitlab/merge_requests/mergeability/results_store_spec.rb'
- './spec/lib/gitlab/metrics/background_transaction_spec.rb'
- './spec/lib/gitlab/metrics/boot_time_tracker_spec.rb'
-- './spec/lib/gitlab/metrics/dashboard/cache_spec.rb'
-- './spec/lib/gitlab/metrics/dashboard/defaults_spec.rb'
-- './spec/lib/gitlab/metrics/dashboard/finder_spec.rb'
-- './spec/lib/gitlab/metrics/dashboard/importer_spec.rb'
-- './spec/lib/gitlab/metrics/dashboard/importers/prometheus_metrics_spec.rb'
-- './spec/lib/gitlab/metrics/dashboard/processor_spec.rb'
-- './spec/lib/gitlab/metrics/dashboard/repo_dashboard_finder_spec.rb'
-- './spec/lib/gitlab/metrics/dashboard/service_selector_spec.rb'
-- './spec/lib/gitlab/metrics/dashboard/stages/grafana_formatter_spec.rb'
-- './spec/lib/gitlab/metrics/dashboard/stages/metric_endpoint_inserter_spec.rb'
-- './spec/lib/gitlab/metrics/dashboard/stages/panel_ids_inserter_spec.rb'
-- './spec/lib/gitlab/metrics/dashboard/stages/track_panel_type_spec.rb'
-- './spec/lib/gitlab/metrics/dashboard/stages/url_validator_spec.rb'
-- './spec/lib/gitlab/metrics/dashboard/stages/variable_endpoint_inserter_spec.rb'
-- './spec/lib/gitlab/metrics/dashboard/transformers/yml/v1/prometheus_metrics_spec.rb'
-- './spec/lib/gitlab/metrics/dashboard/url_spec.rb'
-- './spec/lib/gitlab/metrics/dashboard/validator/client_spec.rb'
-- './spec/lib/gitlab/metrics/dashboard/validator/custom_formats_spec.rb'
-- './spec/lib/gitlab/metrics/dashboard/validator/errors_spec.rb'
-- './spec/lib/gitlab/metrics/dashboard/validator/post_schema_validator_spec.rb'
-- './spec/lib/gitlab/metrics/dashboard/validator_spec.rb'
- './spec/lib/gitlab/metrics/delta_spec.rb'
- './spec/lib/gitlab/metrics/elasticsearch_rack_middleware_spec.rb'
- './spec/lib/gitlab/metrics/exporter/base_exporter_spec.rb'
@@ -6767,7 +6546,6 @@
- './spec/lib/gitlab/optimistic_locking_spec.rb'
- './spec/lib/gitlab/other_markup_spec.rb'
- './spec/lib/gitlab/otp_key_rotator_spec.rb'
-- './spec/lib/gitlab/pages/cache_control_spec.rb'
- './spec/lib/gitlab/pages/deployment_update_spec.rb'
- './spec/lib/gitlab/pages/settings_spec.rb'
- './spec/lib/gitlab/pages_spec.rb'
@@ -6794,7 +6572,6 @@
- './spec/lib/gitlab/pagination/offset_header_builder_spec.rb'
- './spec/lib/gitlab/pagination/offset_header_builder_with_controller_spec.rb'
- './spec/lib/gitlab/pagination/offset_pagination_spec.rb'
-- './spec/lib/gitlab/patch/action_cable_redis_listener_spec.rb'
- './spec/lib/gitlab/patch/database_config_spec.rb'
- './spec/lib/gitlab/patch/draw_route_spec.rb'
- './spec/lib/gitlab/patch/prependable_spec.rb'
@@ -6818,16 +6595,8 @@
- './spec/lib/gitlab/project_template_spec.rb'
- './spec/lib/gitlab/project_transfer_spec.rb'
- './spec/lib/gitlab/prometheus/adapter_spec.rb'
-- './spec/lib/gitlab/prometheus/additional_metrics_parser_spec.rb'
- './spec/lib/gitlab/prometheus_client_spec.rb'
- './spec/lib/gitlab/prometheus/internal_spec.rb'
-- './spec/lib/gitlab/prometheus/metric_group_spec.rb'
-- './spec/lib/gitlab/prometheus/queries/additional_metrics_deployment_query_spec.rb'
-- './spec/lib/gitlab/prometheus/queries/additional_metrics_environment_query_spec.rb'
-- './spec/lib/gitlab/prometheus/queries/deployment_query_spec.rb'
-- './spec/lib/gitlab/prometheus/queries/matched_metric_query_spec.rb'
-- './spec/lib/gitlab/prometheus/queries/validate_query_spec.rb'
-- './spec/lib/gitlab/prometheus/query_variables_spec.rb'
- './spec/lib/gitlab/protocol_access_spec.rb'
- './spec/lib/gitlab/puma_logging/json_formatter_spec.rb'
- './spec/lib/gitlab/push_options_spec.rb'
@@ -6880,7 +6649,6 @@
- './spec/lib/gitlab/robots_txt/parser_spec.rb'
- './spec/lib/gitlab/route_map_spec.rb'
- './spec/lib/gitlab/routing_spec.rb'
-- './spec/lib/gitlab/rugged_instrumentation_spec.rb'
- './spec/lib/gitlab/runtime_spec.rb'
- './spec/lib/gitlab/saas_spec.rb'
- './spec/lib/gitlab/safe_request_loader_spec.rb'
@@ -6918,7 +6686,6 @@
- './spec/lib/gitlab/sidekiq_config/worker_matcher_spec.rb'
- './spec/lib/gitlab/sidekiq_config/worker_router_spec.rb'
- './spec/lib/gitlab/sidekiq_config/worker_spec.rb'
-- './spec/lib/gitlab/sidekiq_daemon/memory_killer_spec.rb'
- './spec/lib/gitlab/sidekiq_daemon/monitor_spec.rb'
- './spec/lib/gitlab/sidekiq_death_handler_spec.rb'
- './spec/lib/gitlab/sidekiq_logging/deduplication_logger_spec.rb'
@@ -7006,7 +6773,6 @@
- './spec/lib/gitlab/template/gitlab_ci_yml_template_spec.rb'
- './spec/lib/gitlab/template/issue_template_spec.rb'
- './spec/lib/gitlab/template/merge_request_template_spec.rb'
-- './spec/lib/gitlab/template/metrics_dashboard_template_spec.rb'
- './spec/lib/gitlab/template_parser/ast_spec.rb'
- './spec/lib/gitlab/template_parser/parser_spec.rb'
- './spec/lib/gitlab/terraform_registry_token_spec.rb'
@@ -7104,7 +6870,6 @@
- './spec/lib/gitlab/verify/job_artifacts_spec.rb'
- './spec/lib/gitlab/verify/lfs_objects_spec.rb'
- './spec/lib/gitlab/verify/uploads_spec.rb'
-- './spec/lib/gitlab/version_info_spec.rb'
- './spec/lib/gitlab/view/presenter/base_spec.rb'
- './spec/lib/gitlab/view/presenter/delegated_spec.rb'
- './spec/lib/gitlab/view/presenter/factory_spec.rb'
@@ -7164,9 +6929,7 @@
- './spec/lib/peek/views/external_http_spec.rb'
- './spec/lib/peek/views/memory_spec.rb'
- './spec/lib/peek/views/redis_detailed_spec.rb'
-- './spec/lib/peek/views/rugged_spec.rb'
- './spec/lib/product_analytics/event_params_spec.rb'
-- './spec/lib/product_analytics/tracker_spec.rb'
- './spec/lib/prometheus/cleanup_multiproc_dir_service_spec.rb'
- './spec/lib/prometheus/pid_provider_spec.rb'
- './spec/lib/quality/seeders/issues_spec.rb'
@@ -7254,75 +7017,7 @@
- './spec/mailers/notify_spec.rb'
- './spec/mailers/repository_check_mailer_spec.rb'
- './spec/metrics_server/metrics_server_spec.rb'
-- './spec/migrations/20220315171129_cleanup_draft_data_from_faulty_regex_spec.rb'
-- './spec/migrations/20220316202640_populate_container_repositories_migration_plan_spec.rb'
-- './spec/migrations/20220321234317_remove_all_issuable_escalation_statuses_spec.rb'
-- './spec/migrations/20220322132242_update_pages_onboarding_state_spec.rb'
-- './spec/migrations/20220324032250_migrate_shimo_confluence_service_category_spec.rb'
-- './spec/migrations/20220324165436_schedule_backfill_project_settings_spec.rb'
-- './spec/migrations/20220329175119_remove_leftover_ci_job_artifact_deletions_spec.rb'
-- './spec/migrations/20220331133802_schedule_backfill_topics_title_spec.rb'
-- './spec/migrations/20220412143552_consume_remaining_encrypt_integration_property_jobs_spec.rb'
-- './spec/migrations/20220416054011_schedule_backfill_project_member_namespace_id_spec.rb'
-- './spec/migrations/20220420135946_update_batched_background_migration_arguments_spec.rb'
-- './spec/migrations/20220426185933_backfill_deployments_finished_at_spec.rb'
-- './spec/migrations/20220502015011_clean_up_fix_merge_request_diff_commit_users_spec.rb'
-- './spec/migrations/20220502173045_reset_too_many_tags_skipped_registry_imports_spec.rb'
-- './spec/migrations/20220503035221_add_gitlab_schema_to_batched_background_migrations_spec.rb'
-- './spec/migrations/20220505044348_fix_automatic_iterations_cadences_start_date_spec.rb'
-- './spec/migrations/20220505174658_update_index_on_alerts_to_exclude_null_fingerprints_spec.rb'
-- './spec/migrations/20220506154054_create_sync_namespace_details_trigger_spec.rb'
-- './spec/migrations/20220512190659_remove_web_hooks_web_hook_logs_web_hook_id_fk_spec.rb'
-- './spec/migrations/20220513043344_reschedule_expire_o_auth_tokens_spec.rb'
-- './spec/migrations/20220523171107_drop_deploy_tokens_token_column_spec.rb'
-- './spec/migrations/20220524074947_finalize_backfill_null_note_discussion_ids_spec.rb'
-- './spec/migrations/20220524184149_create_sync_project_namespace_details_trigger_spec.rb'
-- './spec/migrations/20220525221133_schedule_backfill_vulnerability_reads_cluster_agent_spec.rb'
-- './spec/migrations/20220601110011_schedule_remove_self_managed_wiki_notes_spec.rb'
-- './spec/migrations/20220601152916_add_user_id_and_ip_address_success_index_to_authentication_events_spec.rb'
-- './spec/migrations/20220606082910_add_tmp_index_for_potentially_misassociated_vulnerability_occurrences_spec.rb'
-- './spec/migrations/20220607082910_add_sync_tmp_index_for_potentially_misassociated_vulnerability_occurrences_spec.rb'
-- './spec/migrations/20220620132300_update_last_run_date_for_iterations_cadences_spec.rb'
-- './spec/migrations/20220622080547_backfill_project_statistics_with_container_registry_size_spec.rb'
-- './spec/migrations/20220627090231_schedule_disable_legacy_open_source_license_for_inactive_public_projects_spec.rb'
-- './spec/migrations/20220627152642_queue_update_delayed_project_removal_to_null_for_user_namespace_spec.rb'
-- './spec/migrations/20220628012902_finalise_project_namespace_members_spec.rb'
-- './spec/migrations/20220715163254_update_notes_in_past_spec.rb'
-- './spec/migrations/20220721031446_schedule_disable_legacy_open_source_license_for_one_member_no_repo_projects_spec.rb'
-- './spec/migrations/20220722084543_schedule_disable_legacy_open_source_license_for_no_issues_no_repo_projects_spec.rb'
-- './spec/migrations/20220722110026_reschedule_set_legacy_open_source_license_available_for_non_public_projects_spec.rb'
-- './spec/migrations/20220725150127_update_jira_tracker_data_deployment_type_based_on_url_spec.rb'
-- './spec/migrations/20220801155858_schedule_disable_legacy_open_source_licence_for_recent_public_projects_spec.rb'
-- './spec/migrations/20220802114351_reschedule_backfill_container_registry_size_into_project_statistics_spec.rb'
-- './spec/migrations/20220802204737_remove_deactivated_user_highest_role_stats_spec.rb'
- './spec/migrations/active_record/schema_spec.rb'
-- './spec/migrations/add_epics_relative_position_spec.rb'
-- './spec/migrations/add_web_hook_calls_to_plan_limits_paid_tiers_spec.rb'
-- './spec/migrations/backfill_integrations_enable_ssl_verification_spec.rb'
-- './spec/migrations/backfill_namespace_id_for_project_routes_spec.rb'
-- './spec/migrations/backfill_project_import_level_spec.rb'
-- './spec/migrations/bulk_insert_cluster_enabled_grants_spec.rb'
-- './spec/migrations/change_public_projects_cost_factor_spec.rb'
-- './spec/migrations/cleanup_after_fixing_issue_when_admin_changed_primary_email_spec.rb'
-- './spec/migrations/cleanup_after_fixing_regression_with_new_users_emails_spec.rb'
-- './spec/migrations/cleanup_backfill_integrations_enable_ssl_verification_spec.rb'
-- './spec/migrations/cleanup_mr_attention_request_todos_spec.rb'
-- './spec/migrations/cleanup_orphaned_routes_spec.rb'
-- './spec/migrations/finalize_orphaned_routes_cleanup_spec.rb'
-- './spec/migrations/finalize_project_namespaces_backfill_spec.rb'
-- './spec/migrations/finalize_routes_backfilling_for_projects_spec.rb'
-- './spec/migrations/fix_and_backfill_project_namespaces_for_projects_with_duplicate_name_spec.rb'
-- './spec/migrations/populate_operation_visibility_permissions_spec.rb'
-- './spec/migrations/queue_backfill_project_feature_package_registry_access_level_spec.rb'
-- './spec/migrations/remove_invalid_integrations_spec.rb'
-- './spec/migrations/remove_wiki_notes_spec.rb'
-- './spec/migrations/reschedule_backfill_imported_issue_search_data_spec.rb'
-- './spec/migrations/schedule_backfill_draft_status_on_merge_requests_corrected_regex_spec.rb'
-- './spec/migrations/schedule_backfilling_the_namespace_id_for_vulnerability_reads_spec.rb'
-- './spec/migrations/schedule_populate_requirements_issue_id_spec.rb'
-- './spec/migrations/schedule_purging_stale_security_scans_spec.rb'
-- './spec/migrations/schedule_set_correct_vulnerability_state_spec.rb'
-- './spec/migrations/toggle_vsa_aggregations_enable_spec.rb'
- './spec/models/ability_spec.rb'
- './spec/models/abuse_report_spec.rb'
- './spec/models/active_session_spec.rb'
@@ -7361,7 +7056,6 @@
- './spec/models/blob_viewer/go_mod_spec.rb'
- './spec/models/blob_viewer/license_spec.rb'
- './spec/models/blob_viewer/markup_spec.rb'
-- './spec/models/blob_viewer/metrics_dashboard_yml_spec.rb'
- './spec/models/blob_viewer/package_json_spec.rb'
- './spec/models/blob_viewer/podspec_json_spec.rb'
- './spec/models/blob_viewer/podspec_spec.rb'
@@ -7530,7 +7224,6 @@
- './spec/models/concerns/project_api_compatibility_spec.rb'
- './spec/models/concerns/project_features_compatibility_spec.rb'
- './spec/models/concerns/prometheus_adapter_spec.rb'
-- './spec/models/concerns/protected_ref_access_spec.rb'
- './spec/models/concerns/reactive_caching_spec.rb'
- './spec/models/concerns/redactable_spec.rb'
- './spec/models/concerns/redis_cacheable_spec.rb'
@@ -7615,7 +7308,6 @@
- './spec/models/event_spec.rb'
- './spec/models/exported_protected_branch_spec.rb'
- './spec/models/external_issue_spec.rb'
-- './spec/models/external_pull_request_spec.rb'
- './spec/models/fork_network_member_spec.rb'
- './spec/models/fork_network_spec.rb'
- './spec/models/generic_commit_status_spec.rb'
@@ -7745,8 +7437,6 @@
- './spec/models/merge_request/metrics_spec.rb'
- './spec/models/merge_request_reviewer_spec.rb'
- './spec/models/merge_request_spec.rb'
-- './spec/models/metrics/dashboard/annotation_spec.rb'
-- './spec/models/metrics/users_starred_dashboard_spec.rb'
- './spec/models/milestone_note_spec.rb'
- './spec/models/milestone_release_spec.rb'
- './spec/models/milestone_spec.rb'
@@ -7818,10 +7508,6 @@
- './spec/models/pages_domain_spec.rb'
- './spec/models/pages/lookup_path_spec.rb'
- './spec/models/pages/virtual_domain_spec.rb'
-- './spec/models/performance_monitoring/prometheus_dashboard_spec.rb'
-- './spec/models/performance_monitoring/prometheus_metric_spec.rb'
-- './spec/models/performance_monitoring/prometheus_panel_group_spec.rb'
-- './spec/models/performance_monitoring/prometheus_panel_spec.rb'
- './spec/models/personal_access_token_spec.rb'
- './spec/models/personal_snippet_spec.rb'
- './spec/models/plan_limits_spec.rb'
@@ -7847,13 +7533,10 @@
- './spec/models/project_deploy_token_spec.rb'
- './spec/models/project_export_job_spec.rb'
- './spec/models/project_feature_spec.rb'
-- './spec/models/project_feature_usage_spec.rb'
- './spec/models/project_group_link_spec.rb'
- './spec/models/project_import_data_spec.rb'
- './spec/models/project_import_state_spec.rb'
- './spec/models/project_label_spec.rb'
-- './spec/models/project_metrics_setting_spec.rb'
-- './spec/models/project_pages_metadatum_spec.rb'
- './spec/models/project_repository_spec.rb'
- './spec/models/projects/build_artifacts_size_refresh_spec.rb'
- './spec/models/projects/ci_feature_usage_spec.rb'
@@ -7922,7 +7605,6 @@
- './spec/models/token_with_iv_spec.rb'
- './spec/models/tree_spec.rb'
- './spec/models/trending_project_spec.rb'
-- './spec/models/u2f_registration_spec.rb'
- './spec/models/uploads/fog_spec.rb'
- './spec/models/uploads/local_spec.rb'
- './spec/models/upload_spec.rb'
@@ -8005,7 +7687,6 @@
- './spec/policies/issuable_policy_spec.rb'
- './spec/policies/issue_policy_spec.rb'
- './spec/policies/merge_request_policy_spec.rb'
-- './spec/policies/metrics/dashboard/annotation_policy_spec.rb'
- './spec/policies/namespace/root_storage_statistics_policy_spec.rb'
- './spec/policies/namespaces/project_namespace_policy_spec.rb'
- './spec/policies/namespaces/user_namespace_policy_spec.rb'
@@ -8064,7 +7745,6 @@
- './spec/presenters/packages/conan/package_presenter_spec.rb'
- './spec/presenters/packages/detail/package_presenter_spec.rb'
- './spec/presenters/packages/helm/index_presenter_spec.rb'
-- './spec/presenters/packages/npm/package_presenter_spec.rb'
- './spec/presenters/packages/nuget/package_metadata_presenter_spec.rb'
- './spec/presenters/packages/nuget/packages_metadata_presenter_spec.rb'
- './spec/presenters/packages/nuget/packages_versions_presenter_spec.rb'
@@ -8214,8 +7894,6 @@
- './spec/requests/api/graphql/issue_status_counts_spec.rb'
- './spec/requests/api/graphql/merge_request/merge_request_spec.rb'
- './spec/requests/api/graphql/metadata_query_spec.rb'
-- './spec/requests/api/graphql/metrics/dashboard/annotations_spec.rb'
-- './spec/requests/api/graphql/metrics/dashboard_query_spec.rb'
- './spec/requests/api/graphql/milestone_spec.rb'
- './spec/requests/api/graphql/multiplexed_queries_spec.rb'
- './spec/requests/api/graphql/mutations/admin/sidekiq_queues/delete_jobs_spec.rb'
@@ -8342,7 +8020,6 @@
- './spec/requests/api/graphql/packages/pypi_spec.rb'
- './spec/requests/api/graphql/project/alert_management/alert/assignees_spec.rb'
- './spec/requests/api/graphql/project/alert_management/alert/issue_spec.rb'
-- './spec/requests/api/graphql/project/alert_management/alert/metrics_dashboard_url_spec.rb'
- './spec/requests/api/graphql/project/alert_management/alert/notes_spec.rb'
- './spec/requests/api/graphql/project/alert_management/alerts_spec.rb'
- './spec/requests/api/graphql/project/alert_management/alert_status_counts_spec.rb'
@@ -8520,7 +8197,6 @@
- './spec/requests/api/user_counts_spec.rb'
- './spec/requests/api/users_preferences_spec.rb'
- './spec/requests/api/users_spec.rb'
-- './spec/requests/api/v3/github_spec.rb'
- './spec/requests/api/wikis_spec.rb'
- './spec/requests/concerns/planning_hierarchy_spec.rb'
- './spec/requests/content_security_policy_spec.rb'
@@ -8544,7 +8220,6 @@
- './spec/requests/import/gitlab_groups_controller_spec.rb'
- './spec/requests/import/gitlab_projects_controller_spec.rb'
- './spec/requests/import/url_controller_spec.rb'
-- './spec/requests/jira_authorizations_spec.rb'
- './spec/requests/jira_connect/installations_controller_spec.rb'
- './spec/requests/jira_connect/oauth_application_ids_controller_spec.rb'
- './spec/requests/jira_connect/oauth_callbacks_controller_spec.rb'
@@ -8588,9 +8263,6 @@
- './spec/requests/projects/merge_requests/diffs_spec.rb'
- './spec/requests/projects/merge_requests_discussions_spec.rb'
- './spec/requests/projects/merge_requests_spec.rb'
-- './spec/requests/projects/metrics/dashboards/builder_spec.rb'
-- './spec/requests/projects/metrics_dashboard_spec.rb'
-- './spec/requests/projects/noteable_notes_spec.rb'
- './spec/requests/projects/pipelines_controller_spec.rb'
- './spec/requests/projects/redirect_controller_spec.rb'
- './spec/requests/projects/releases_controller_spec.rb'
@@ -8786,7 +8458,6 @@
- './spec/serializers/project_mirror_serializer_spec.rb'
- './spec/serializers/project_note_entity_spec.rb'
- './spec/serializers/project_serializer_spec.rb'
-- './spec/serializers/prometheus_alert_entity_spec.rb'
- './spec/serializers/release_serializer_spec.rb'
- './spec/serializers/remote_mirror_entity_spec.rb'
- './spec/serializers/request_aware_entity_spec.rb'
@@ -8861,7 +8532,6 @@
- './spec/services/branches/validate_new_service_spec.rb'
- './spec/services/bulk_create_integration_service_spec.rb'
- './spec/services/bulk_imports/archive_extraction_service_spec.rb'
-- './spec/services/bulk_imports/create_pipeline_trackers_service_spec.rb'
- './spec/services/bulk_imports/create_service_spec.rb'
- './spec/services/bulk_imports/export_service_spec.rb'
- './spec/services/bulk_imports/file_decompression_service_spec.rb'
@@ -9028,10 +8698,8 @@
- './spec/services/dependency_proxy/request_token_service_spec.rb'
- './spec/services/deploy_keys/create_service_spec.rb'
- './spec/services/deployments/archive_in_project_service_spec.rb'
-- './spec/services/deployments/create_for_build_service_spec.rb'
- './spec/services/deployments/create_service_spec.rb'
- './spec/services/deployments/link_merge_requests_service_spec.rb'
-- './spec/services/deployments/older_deployments_drop_service_spec.rb'
- './spec/services/deployments/update_environment_service_spec.rb'
- './spec/services/deployments/update_service_spec.rb'
- './spec/services/design_management/copy_design_collection/copy_service_spec.rb'
@@ -9089,7 +8757,6 @@
- './spec/services/google_cloud/setup_cloudsql_instance_service_spec.rb'
- './spec/services/gpg_keys/create_service_spec.rb'
- './spec/services/gpg_keys/destroy_service_spec.rb'
-- './spec/services/grafana/proxy_service_spec.rb'
- './spec/services/gravatar_service_spec.rb'
- './spec/services/groups/autocomplete_service_spec.rb'
- './spec/services/groups/auto_devops_service_spec.rb'
@@ -9212,7 +8879,6 @@
- './spec/services/merge_requests/delete_non_latest_diffs_service_spec.rb'
- './spec/services/merge_requests/execute_approval_hooks_service_spec.rb'
- './spec/services/merge_requests/export_csv_service_spec.rb'
-- './spec/services/merge_requests/ff_merge_service_spec.rb'
- './spec/services/merge_requests/get_urls_service_spec.rb'
- './spec/services/merge_requests/handle_assignees_change_service_spec.rb'
- './spec/services/merge_requests/link_lfs_objects_service_spec.rb'
@@ -9246,25 +8912,6 @@
- './spec/services/merge_requests/update_assignees_service_spec.rb'
- './spec/services/merge_requests/update_reviewers_service_spec.rb'
- './spec/services/merge_requests/update_service_spec.rb'
-- './spec/services/metrics/dashboard/annotations/create_service_spec.rb'
-- './spec/services/metrics/dashboard/annotations/delete_service_spec.rb'
-- './spec/services/metrics/dashboard/clone_dashboard_service_spec.rb'
-- './spec/services/metrics/dashboard/cluster_dashboard_service_spec.rb'
-- './spec/services/metrics/dashboard/cluster_metrics_embed_service_spec.rb'
-- './spec/services/metrics/dashboard/custom_dashboard_service_spec.rb'
-- './spec/services/metrics/dashboard/custom_metric_embed_service_spec.rb'
-- './spec/services/metrics/dashboard/default_embed_service_spec.rb'
-- './spec/services/metrics/dashboard/dynamic_embed_service_spec.rb'
-- './spec/services/metrics/dashboard/gitlab_alert_embed_service_spec.rb'
-- './spec/services/metrics/dashboard/grafana_metric_embed_service_spec.rb'
-- './spec/services/metrics/dashboard/panel_preview_service_spec.rb'
-- './spec/services/metrics/dashboard/pod_dashboard_service_spec.rb'
-- './spec/services/metrics/dashboard/system_dashboard_service_spec.rb'
-- './spec/services/metrics/dashboard/transient_embed_service_spec.rb'
-- './spec/services/metrics/dashboard/update_dashboard_service_spec.rb'
-- './spec/services/metrics/sample_metrics_service_spec.rb'
-- './spec/services/metrics/users_starred_dashboards/create_service_spec.rb'
-- './spec/services/metrics/users_starred_dashboards/delete_service_spec.rb'
- './spec/services/milestones/closed_issues_count_service_spec.rb'
- './spec/services/milestones/close_service_spec.rb'
- './spec/services/milestones/create_service_spec.rb'
@@ -9310,11 +8957,9 @@
- './spec/services/packages/debian/extract_deb_metadata_service_spec.rb'
- './spec/services/packages/debian/extract_metadata_service_spec.rb'
- './spec/services/packages/debian/find_or_create_incoming_service_spec.rb'
-- './spec/services/packages/debian/find_or_create_package_service_spec.rb'
- './spec/services/packages/debian/generate_distribution_key_service_spec.rb'
- './spec/services/packages/debian/generate_distribution_service_spec.rb'
- './spec/services/packages/debian/parse_debian822_service_spec.rb'
-- './spec/services/packages/debian/process_changes_service_spec.rb'
- './spec/services/packages/debian/sign_distribution_service_spec.rb'
- './spec/services/packages/debian/update_distribution_service_spec.rb'
- './spec/services/packages/generic/create_package_file_service_spec.rb'
@@ -9353,7 +8998,6 @@
- './spec/services/pages_domains/create_acme_order_service_spec.rb'
- './spec/services/pages_domains/obtain_lets_encrypt_certificate_service_spec.rb'
- './spec/services/pages_domains/retry_acme_order_service_spec.rb'
-- './spec/services/pages/zip_directory_service_spec.rb'
- './spec/services/personal_access_tokens/create_service_spec.rb'
- './spec/services/personal_access_tokens/last_used_service_spec.rb'
- './spec/services/personal_access_tokens/revoke_service_spec.rb'
@@ -9395,7 +9039,6 @@
- './spec/services/projects/group_links/update_service_spec.rb'
- './spec/services/projects/hashed_storage/base_attachment_service_spec.rb'
- './spec/services/projects/hashed_storage/migrate_attachments_service_spec.rb'
-- './spec/services/projects/hashed_storage/migrate_repository_service_spec.rb'
- './spec/services/projects/hashed_storage/migration_service_spec.rb'
- './spec/services/projects/import_error_filter_spec.rb'
- './spec/services/projects/import_export/export_service_spec.rb'
@@ -9435,8 +9078,6 @@
- './spec/services/projects/update_repository_storage_service_spec.rb'
- './spec/services/projects/update_service_spec.rb'
- './spec/services/projects/update_statistics_service_spec.rb'
-- './spec/services/prometheus/proxy_service_spec.rb'
-- './spec/services/prometheus/proxy_variable_substitution_service_spec.rb'
- './spec/services/protected_branches/cache_service_spec.rb'
- './spec/services/protected_branches/create_service_spec.rb'
- './spec/services/protected_branches/destroy_service_spec.rb'
@@ -9560,7 +9201,6 @@
- './spec/services/users/saved_replies/destroy_service_spec.rb'
- './spec/services/users/saved_replies/update_service_spec.rb'
- './spec/services/users/set_status_service_spec.rb'
-- './spec/services/users/signup_service_spec.rb'
- './spec/services/users/unban_service_spec.rb'
- './spec/services/users/update_canonical_email_service_spec.rb'
- './spec/services/users/update_highest_member_role_service_spec.rb'
@@ -9614,8 +9254,6 @@
- './spec/support_specs/matchers/be_sorted_spec.rb'
- './spec/support_specs/matchers/exceed_query_limit_helpers_spec.rb'
- './spec/tasks/admin_mode_spec.rb'
-- './spec/tasks/cache/clear/redis_spec.rb'
-- './spec/tasks/config_lint_spec.rb'
- './spec/tasks/dev_rake_spec.rb'
- './spec/tasks/gitlab/artifacts/check_rake_spec.rb'
- './spec/tasks/gitlab/artifacts/migrate_rake_spec.rb'
@@ -9630,13 +9268,11 @@
- './spec/tasks/gitlab/db/validate_config_rake_spec.rb'
- './spec/tasks/gitlab/dependency_proxy/migrate_rake_spec.rb'
- './spec/tasks/gitlab/external_diffs_rake_spec.rb'
-- './spec/tasks/gitlab/generate_sample_prometheus_data_spec.rb'
- './spec/tasks/gitlab/gitaly_rake_spec.rb'
- './spec/tasks/gitlab/git_rake_spec.rb'
- './spec/tasks/gitlab/ldap_rake_spec.rb'
- './spec/tasks/gitlab/lfs/check_rake_spec.rb'
- './spec/tasks/gitlab/lfs/migrate_rake_spec.rb'
-- './spec/tasks/gitlab/packages/events_rake_spec.rb'
- './spec/tasks/gitlab/packages/migrate_rake_spec.rb'
- './spec/tasks/gitlab/pages_rake_spec.rb'
- './spec/tasks/gitlab/password_rake_spec.rb'
@@ -9648,7 +9284,6 @@
- './spec/tasks/gitlab/sidekiq_rake_spec.rb'
- './spec/tasks/gitlab/smtp_rake_spec.rb'
- './spec/tasks/gitlab/snippets_rake_spec.rb'
-- './spec/tasks/gitlab/task_helpers_spec.rb'
- './spec/tasks/gitlab/terraform/migrate_rake_spec.rb'
- './spec/tasks/gitlab/update_templates_rake_spec.rb'
- './spec/tasks/gitlab/uploads/check_rake_spec.rb'
@@ -9660,14 +9295,6 @@
- './spec/tasks/gitlab/x509/update_rake_spec.rb'
- './spec/tasks/migrate/schema_check_rake_spec.rb'
- './spec/tasks/rubocop_rake_spec.rb'
-- './spec/tasks/tokens_spec.rb'
-- './spec/tooling/danger/customer_success_spec.rb'
-- './spec/tooling/danger/datateam_spec.rb'
-- './spec/tooling/danger/feature_flag_spec.rb'
-- './spec/tooling/danger/analytics_instrumentation_spec.rb'
-- './spec/tooling/danger/project_helper_spec.rb'
-- './spec/tooling/danger/sidekiq_queues_spec.rb'
-- './spec/tooling/danger/specs_spec.rb'
- './spec/tooling/docs/deprecation_handling_spec.rb'
- './spec/tooling/graphql/docs/renderer_spec.rb'
- './spec/tooling/lib/tooling/crystalball/coverage_lines_execution_detector_spec.rb'
@@ -9817,7 +9444,6 @@
- './spec/views/projects/hooks/edit.html.haml_spec.rb'
- './spec/views/projects/hooks/index.html.haml_spec.rb'
- './spec/views/projects/imports/new.html.haml_spec.rb'
-- './spec/views/projects/issues/_issue.html.haml_spec.rb'
- './spec/views/projects/issues/_related_branches.html.haml_spec.rb'
- './spec/views/projects/issues/_service_desk_info_content.html.haml_spec.rb'
- './spec/views/projects/issues/show.html.haml_spec.rb'
@@ -9832,7 +9458,6 @@
- './spec/views/projects/pages_domains/show.html.haml_spec.rb'
- './spec/views/projects/pages/new.html.haml_spec.rb'
- './spec/views/projects/pages/show.html.haml_spec.rb'
-- './spec/views/projects/pipeline_schedules/_pipeline_schedule.html.haml_spec.rb'
- './spec/views/projects/pipelines/show.html.haml_spec.rb'
- './spec/views/projects/project_members/index.html.haml_spec.rb'
- './spec/views/projects/settings/ci_cd/_autodevops_form.html.haml_spec.rb'
@@ -9840,7 +9465,6 @@
- './spec/views/projects/settings/operations/show.html.haml_spec.rb'
- './spec/views/projects/tags/index.html.haml_spec.rb'
- './spec/views/projects/tree/show.html.haml_spec.rb'
-- './spec/views/registrations/welcome/show.html.haml_spec.rb'
- './spec/views/search/_results.html.haml_spec.rb'
- './spec/views/search/show.html.haml_spec.rb'
- './spec/views/shared/groups/_dropdown.html.haml_spec.rb'
@@ -9851,7 +9475,6 @@
- './spec/views/shared/_milestones_sort_dropdown.html.haml_spec.rb'
- './spec/views/shared/milestones/_top.html.haml_spec.rb'
- './spec/views/shared/nav/_sidebar.html.haml_spec.rb'
-- './spec/views/shared/notes/_form.html.haml_spec.rb'
- './spec/views/shared/projects/_inactive_project_deletion_alert.html.haml_spec.rb'
- './spec/views/shared/projects/_list.html.haml_spec.rb'
- './spec/views/shared/projects/_project.html.haml_spec.rb'
@@ -9875,7 +9498,6 @@
- './spec/workers/background_migration_worker_spec.rb'
- './spec/workers/build_hooks_worker_spec.rb'
- './spec/workers/build_queue_worker_spec.rb'
-- './spec/workers/build_success_worker_spec.rb'
- './spec/workers/bulk_imports/entity_worker_spec.rb'
- './spec/workers/bulk_imports/export_request_worker_spec.rb'
- './spec/workers/bulk_imports/pipeline_worker_spec.rb'
@@ -9924,21 +9546,18 @@
- './spec/workers/clusters/applications/deactivate_integration_worker_spec.rb'
- './spec/workers/clusters/cleanup/project_namespace_worker_spec.rb'
- './spec/workers/clusters/cleanup/service_account_worker_spec.rb'
-- './spec/workers/clusters/integrations/check_prometheus_health_worker_spec.rb'
- './spec/workers/concerns/application_worker_spec.rb'
- './spec/workers/concerns/cluster_agent_queue_spec.rb'
- './spec/workers/concerns/cronjob_queue_spec.rb'
- './spec/workers/concerns/gitlab/github_import/object_importer_spec.rb'
- './spec/workers/concerns/gitlab/github_import/rescheduling_methods_spec.rb'
- './spec/workers/concerns/gitlab/github_import/stage_methods_spec.rb'
-- './spec/workers/concerns/gitlab/notify_upon_death_spec.rb'
- './spec/workers/concerns/limited_capacity/job_tracker_spec.rb'
- './spec/workers/concerns/limited_capacity/worker_spec.rb'
- './spec/workers/concerns/packages/cleanup_artifact_worker_spec.rb'
- './spec/workers/concerns/project_import_options_spec.rb'
- './spec/workers/concerns/reenqueuer_spec.rb'
- './spec/workers/concerns/repository_check_queue_spec.rb'
-- './spec/workers/concerns/waitable_worker_spec.rb'
- './spec/workers/concerns/worker_attributes_spec.rb'
- './spec/workers/concerns/worker_context_spec.rb'
- './spec/workers/container_expiration_policies/cleanup_container_repository_worker_spec.rb'
@@ -9955,7 +9574,6 @@
- './spec/workers/database/ci_project_mirrors_consistency_check_worker_spec.rb'
- './spec/workers/database/drop_detached_partitions_worker_spec.rb'
- './spec/workers/database/partition_management_worker_spec.rb'
-- './spec/workers/delete_container_repository_worker_spec.rb'
- './spec/workers/delete_diff_files_worker_spec.rb'
- './spec/workers/delete_merged_branches_worker_spec.rb'
- './spec/workers/delete_user_worker_spec.rb'
@@ -9991,8 +9609,6 @@
- './spec/workers/gitlab/github_import/import_issue_event_worker_spec.rb'
- './spec/workers/gitlab/github_import/import_issue_worker_spec.rb'
- './spec/workers/gitlab/github_import/import_note_worker_spec.rb'
-- './spec/workers/gitlab/github_import/import_pull_request_merged_by_worker_spec.rb'
-- './spec/workers/gitlab/github_import/import_pull_request_review_worker_spec.rb'
- './spec/workers/gitlab/github_import/import_pull_request_worker_spec.rb'
- './spec/workers/gitlab/github_import/refresh_import_jid_worker_spec.rb'
- './spec/workers/gitlab/github_import/stage/finish_import_worker_spec.rb'
@@ -10017,7 +9633,6 @@
- './spec/workers/gitlab/jira_import/stuck_jira_import_jobs_worker_spec.rb'
- './spec/workers/gitlab_performance_bar_stats_worker_spec.rb'
- './spec/workers/gitlab_service_ping_worker_spec.rb'
-- './spec/workers/gitlab_shell_worker_spec.rb'
- './spec/workers/google_cloud/create_cloudsql_instance_worker_spec.rb'
- './spec/workers/group_destroy_worker_spec.rb'
- './spec/workers/group_export_worker_spec.rb'
@@ -10061,9 +9676,6 @@
- './spec/workers/merge_requests/resolve_todos_worker_spec.rb'
- './spec/workers/merge_requests/update_head_pipeline_worker_spec.rb'
- './spec/workers/merge_worker_spec.rb'
-- './spec/workers/metrics/dashboard/prune_old_annotations_worker_spec.rb'
-- './spec/workers/metrics/dashboard/schedule_annotations_prune_worker_spec.rb'
-- './spec/workers/metrics/dashboard/sync_dashboards_worker_spec.rb'
- './spec/workers/migrate_external_diffs_worker_spec.rb'
- './spec/workers/namespaces/process_sync_events_worker_spec.rb'
- './spec/workers/namespaces/prune_aggregation_schedules_worker_spec.rb'
@@ -10082,7 +9694,6 @@
- './spec/workers/packages/composer/cache_cleanup_worker_spec.rb'
- './spec/workers/packages/composer/cache_update_worker_spec.rb'
- './spec/workers/packages/debian/generate_distribution_worker_spec.rb'
-- './spec/workers/packages/debian/process_changes_worker_spec.rb'
- './spec/workers/packages/go/sync_packages_worker_spec.rb'
- './spec/workers/packages/helm/extraction_worker_spec.rb'
- './spec/workers/packages/mark_package_files_for_destruction_worker_spec.rb'
@@ -10094,7 +9705,6 @@
- './spec/workers/pages_domain_ssl_renewal_worker_spec.rb'
- './spec/workers/pages_domain_verification_cron_worker_spec.rb'
- './spec/workers/pages_domain_verification_worker_spec.rb'
-- './spec/workers/pages/invalidate_domain_cache_worker_spec.rb'
- './spec/workers/pages_worker_spec.rb'
- './spec/workers/partition_creation_worker_spec.rb'
- './spec/workers/personal_access_tokens/expired_notification_worker_spec.rb'
diff --git a/spec/support/shared_contexts/ci/catalog/resources/version_shared_context.rb b/spec/support/shared_contexts/ci/catalog/resources/version_shared_context.rb
new file mode 100644
index 00000000000..3c9bb980b46
--- /dev/null
+++ b/spec/support/shared_contexts/ci/catalog/resources/version_shared_context.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+RSpec.shared_context 'when there are catalog resources with versions' do
+ let_it_be(:current_user) { create(:user) }
+
+ let_it_be(:project1) { create(:project, :repository) }
+ let_it_be(:project2) { create(:project, :repository) }
+ let_it_be(:project3) { create(:project, :repository) }
+ let_it_be_with_reload(:resource1) { create(:ci_catalog_resource, project: project1) }
+ let_it_be_with_reload(:resource2) { create(:ci_catalog_resource, project: project2) }
+ let_it_be(:resource3) { create(:ci_catalog_resource, project: project3) }
+
+ let_it_be(:release_v1_0) { create(:release, project: project1, tag: 'v1.0', released_at: 4.days.ago) }
+ let_it_be(:release_v1_1) { create(:release, project: project1, tag: 'v1.1', released_at: 3.days.ago) }
+ let_it_be(:release_v2_0) { create(:release, project: project2, tag: 'v2.0', released_at: 2.days.ago) }
+ let_it_be(:release_v2_1) { create(:release, project: project2, tag: 'v2.1', released_at: 1.day.ago) }
+
+ let_it_be(:v1_0) do
+ create(:ci_catalog_resource_version, catalog_resource: resource1, release: release_v1_0, created_at: 1.day.ago)
+ end
+
+ let_it_be(:v1_1) do
+ create(:ci_catalog_resource_version, catalog_resource: resource1, release: release_v1_1, created_at: 2.days.ago)
+ end
+
+ let_it_be(:v2_0) do
+ create(:ci_catalog_resource_version, catalog_resource: resource2, release: release_v2_0, created_at: 3.days.ago)
+ end
+
+ let_it_be(:v2_1) do
+ create(:ci_catalog_resource_version, catalog_resource: resource2, release: release_v2_1, created_at: 4.days.ago)
+ end
+end
diff --git a/spec/support/shared_contexts/controllers/ambiguous_ref_controller_shared_context.rb b/spec/support/shared_contexts/controllers/ambiguous_ref_controller_shared_context.rb
new file mode 100644
index 00000000000..8ad7edee1a1
--- /dev/null
+++ b/spec/support/shared_contexts/controllers/ambiguous_ref_controller_shared_context.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+RSpec.shared_context 'with ambiguous refs for controllers' do
+ let(:ambiguous_ref_modal) { false }
+
+ before do
+ expect(::Gitlab::GitalyClient).to receive(:allow_ref_name_caching).and_call_original # rubocop:disable RSpec/ExpectInHook
+ project.repository.add_tag(project.creator, 'ambiguous_ref', RepoHelpers.sample_commit.id)
+ project.repository.add_branch(project.creator, 'ambiguous_ref', RepoHelpers.another_sample_commit.id)
+
+ stub_feature_flags(redirect_with_ref_type: redirect_with_ref_type)
+ stub_feature_flags(ambiguous_ref_modal: ambiguous_ref_modal)
+ end
+
+ after do
+ project.repository.rm_tag(project.creator, 'ambiguous_ref')
+ project.repository.rm_branch(project.creator, 'ambiguous_ref')
+ end
+end
diff --git a/spec/support/shared_contexts/features/integrations/project_integrations_shared_context.rb b/spec/support/shared_contexts/features/integrations/project_integrations_shared_context.rb
index c3da9435e05..743a7cd26e0 100644
--- a/spec/support/shared_contexts/features/integrations/project_integrations_shared_context.rb
+++ b/spec/support/shared_contexts/features/integrations/project_integrations_shared_context.rb
@@ -4,7 +4,7 @@ RSpec.shared_context 'project integration activation' do
include_context 'with integration activation'
let_it_be(:project) { create(:project) }
- let_it_be(:user) { create(:user, :no_super_sidebar) }
+ let_it_be(:user) { create(:user) }
before do
project.add_maintainer(user)
diff --git a/spec/support/shared_contexts/graphql/resolvers/runners_resolver_shared_context.rb b/spec/support/shared_contexts/graphql/resolvers/runners_resolver_shared_context.rb
index 1480b5f98e7..2dbb903a272 100644
--- a/spec/support/shared_contexts/graphql/resolvers/runners_resolver_shared_context.rb
+++ b/spec/support/shared_contexts/graphql/resolvers/runners_resolver_shared_context.rb
@@ -9,14 +9,14 @@ RSpec.shared_context 'runners resolver setup' do
let_it_be(:project) { create(:project, :public, group: group) }
let_it_be(:inactive_project_runner) do
- create(:ci_runner, :project, projects: [project], description: 'inactive project runner', token: 'abcdef', active: false, contacted_at: 1.minute.ago, tag_list: %w(project_runner))
+ create(:ci_runner, :project, projects: [project], description: 'inactive project runner', token: 'abcdef', active: false, contacted_at: 1.minute.ago, tag_list: %w[project_runner])
end
let_it_be(:offline_project_runner) do
- create(:ci_runner, :project, projects: [project], description: 'offline project runner', token: 'defghi', contacted_at: 1.day.ago, tag_list: %w(project_runner active_runner))
+ create(:ci_runner, :project, projects: [project], description: 'offline project runner', token: 'defghi', contacted_at: 1.day.ago, tag_list: %w[project_runner active_runner])
end
let_it_be(:group_runner) { create(:ci_runner, :group, groups: [group], token: 'mnopqr', description: 'group runner', contacted_at: 2.seconds.ago) }
let_it_be(:subgroup_runner) { create(:ci_runner, :group, groups: [subgroup], token: '123456', description: 'subgroup runner', contacted_at: 1.second.ago) }
- let_it_be(:instance_runner) { create(:ci_runner, :instance, description: 'shared runner', token: 'stuvxz', contacted_at: 2.minutes.ago, tag_list: %w(instance_runner active_runner)) }
+ let_it_be(:instance_runner) { create(:ci_runner, :instance, description: 'shared runner', token: 'stuvxz', contacted_at: 2.minutes.ago, tag_list: %w[instance_runner active_runner]) }
end
diff --git a/spec/support/shared_contexts/graphql/types/query_type_shared_context.rb b/spec/support/shared_contexts/graphql/types/query_type_shared_context.rb
index 434592ccd38..257ccc553fe 100644
--- a/spec/support/shared_contexts/graphql/types/query_type_shared_context.rb
+++ b/spec/support/shared_contexts/graphql/types/query_type_shared_context.rb
@@ -13,6 +13,8 @@ RSpec.shared_context 'with FOSS query type fields' do
:current_user,
:design_management,
:echo,
+ :frecent_groups,
+ :frecent_projects,
:gitpod_enabled,
:group,
:groups,
diff --git a/spec/support/shared_contexts/lib/gitlab/sidekiq_middleware/server_metrics_shared_context.rb b/spec/support/shared_contexts/lib/gitlab/sidekiq_middleware/server_metrics_shared_context.rb
index d9b2b44980c..85ee3ed4183 100644
--- a/spec/support/shared_contexts/lib/gitlab/sidekiq_middleware/server_metrics_shared_context.rb
+++ b/spec/support/shared_contexts/lib/gitlab/sidekiq_middleware/server_metrics_shared_context.rb
@@ -19,6 +19,12 @@ RSpec.shared_context 'server metrics with mocked prometheus' do
let(:load_balancing_metric) { double('load balancing metric') }
let(:sidekiq_mem_total_bytes) { double('sidekiq mem total bytes') }
let(:completion_seconds_sum_metric) { double('sidekiq completion seconds sum metric') }
+ let(:completion_count_metric) { double('sidekiq completion seconds count metric') }
+ let(:cpu_seconds_sum_metric) { double('cpu seconds sum metric') }
+ let(:db_seconds_sum_metric) { double('db seconds sum metric') }
+ let(:gitaly_seconds_sum_metric) { double('gitaly seconds sum metric') }
+ let(:redis_seconds_sum_metric) { double('redis seconds sum metric') }
+ let(:elasticsearch_seconds_sum_metric) { double('elasticsearch seconds sum metric') }
before do
allow(Gitlab::Metrics).to receive(:histogram).and_call_original
@@ -38,6 +44,12 @@ RSpec.shared_context 'server metrics with mocked prometheus' do
allow(Gitlab::Metrics).to receive(:counter).with(:sidekiq_elasticsearch_requests_total, anything).and_return(elasticsearch_requests_total)
allow(Gitlab::Metrics).to receive(:counter).with(:sidekiq_load_balancing_count, anything).and_return(load_balancing_metric)
allow(Gitlab::Metrics).to receive(:counter).with(:sidekiq_jobs_completion_seconds_sum, anything).and_return(completion_seconds_sum_metric)
+ allow(Gitlab::Metrics).to receive(:counter).with(:sidekiq_jobs_completion_count, anything).and_return(completion_count_metric)
+ allow(Gitlab::Metrics).to receive(:counter).with(:sidekiq_jobs_cpu_seconds_sum, anything).and_return(cpu_seconds_sum_metric)
+ allow(Gitlab::Metrics).to receive(:counter).with(:sidekiq_jobs_db_seconds_sum, anything).and_return(db_seconds_sum_metric)
+ allow(Gitlab::Metrics).to receive(:counter).with(:sidekiq_jobs_gitaly_seconds_sum, anything).and_return(gitaly_seconds_sum_metric)
+ allow(Gitlab::Metrics).to receive(:counter).with(:sidekiq_redis_requests_duration_seconds_sum, anything).and_return(redis_seconds_sum_metric)
+ allow(Gitlab::Metrics).to receive(:counter).with(:sidekiq_elasticsearch_requests_duration_seconds_sum, anything).and_return(elasticsearch_seconds_sum_metric)
allow(Gitlab::Metrics).to receive(:gauge).with(:sidekiq_running_jobs, anything, {}, :all).and_return(running_jobs_metric)
allow(Gitlab::Metrics).to receive(:gauge).with(:sidekiq_concurrency, anything, {}, :all).and_return(concurrency_metric)
allow(Gitlab::Metrics).to receive(:gauge).with(:sidekiq_mem_total_bytes, anything, {}, :all).and_return(sidekiq_mem_total_bytes)
@@ -78,12 +90,8 @@ RSpec.shared_context 'server metrics call' do
}
end
- let(:stub_subject) { true }
-
before do
- if stub_subject
- allow(subject).to receive(:get_thread_cputime).and_return(thread_cputime_before, thread_cputime_after)
- end
+ allow(subject).to receive(:get_thread_cputime).and_return(thread_cputime_before, thread_cputime_after)
allow(Gitlab::Metrics::System).to receive(:monotonic_time).and_return(monotonic_time_before, monotonic_time_after)
allow(Gitlab::InstrumentationHelper).to receive(:queue_duration_for_job).with(job).and_return(queue_duration_for_job)
@@ -101,9 +109,16 @@ RSpec.shared_context 'server metrics call' do
allow(redis_requests_total).to receive(:increment)
allow(elasticsearch_requests_total).to receive(:increment)
allow(completion_seconds_sum_metric).to receive(:increment)
+ allow(completion_count_metric).to receive(:increment)
+ allow(cpu_seconds_sum_metric).to receive(:increment)
+ allow(db_seconds_sum_metric).to receive(:increment)
+ allow(gitaly_seconds_sum_metric).to receive(:increment)
+ allow(redis_seconds_sum_metric).to receive(:increment)
+ allow(elasticsearch_seconds_sum_metric).to receive(:increment)
allow(queue_duration_seconds).to receive(:observe)
allow(user_execution_seconds_metric).to receive(:observe)
allow(db_seconds_metric).to receive(:observe)
+ allow(db_seconds_sum_metric).to receive(:increment)
allow(gitaly_seconds_metric).to receive(:observe)
allow(completion_seconds_metric).to receive(:observe)
allow(redis_seconds_metric).to receive(:observe)
diff --git a/spec/support/shared_contexts/models/ci/job_token_scope.rb b/spec/support/shared_contexts/models/ci/job_token_scope.rb
index d0fee23b57c..a33a3e09c71 100644
--- a/spec/support/shared_contexts/models/ci/job_token_scope.rb
+++ b/spec/support/shared_contexts/models/ci/job_token_scope.rb
@@ -16,12 +16,14 @@ end
RSpec.shared_context 'with inaccessible projects' do
let_it_be(:inbound_allowlist_project) { create_project_in_allowlist(source_project, direction: :inbound) }
+
include_context 'with unscoped projects'
end
RSpec.shared_context 'with unscoped projects' do
let_it_be(:unscoped_project1) { create(:project) }
let_it_be(:unscoped_project2) { create(:project) }
+ let_it_be(:unscoped_public_project) { create(:project, :public) }
let_it_be(:link_out_of_scope) { create(:ci_job_token_project_scope_link, target_project: unscoped_project1) }
end
diff --git a/spec/support/shared_contexts/navbar_structure_context.rb b/spec/support/shared_contexts/navbar_structure_context.rb
index a09319b4980..a5ccce27aa5 100644
--- a/spec/support/shared_contexts/navbar_structure_context.rb
+++ b/spec/support/shared_contexts/navbar_structure_context.rb
@@ -3,9 +3,9 @@
RSpec.shared_context 'project navbar structure' do
include NavbarStructureHelper
- let(:security_and_compliance_nav_item) do
+ let(:secure_nav_item) do
{
- nav_item: _('Security and Compliance'),
+ nav_item: _('Secure'),
nav_sub_items: [
(_('Audit events') if Gitlab.ee?),
_('Security configuration')
@@ -16,65 +16,58 @@ RSpec.shared_context 'project navbar structure' do
let(:structure) do
[
{
- nav_item: "#{project.name[0, 1].upcase} #{project.name}",
- nav_sub_items: []
+ nav_item: _('Manage'),
+ nav_sub_items: [
+ _('Activity'),
+ _('Members'),
+ _('Labels')
+ ]
},
{
- nav_item: _('Project information'),
+ nav_item: _('Plan'),
nav_sub_items: [
- _('Activity'),
- _('Labels'),
- _('Members')
+ _('Issues'),
+ _('Issue boards'),
+ _('Milestones'),
+ _('Wiki')
]
},
{
- nav_item: _('Repository'),
+ nav_item: _('Code'),
nav_sub_items: [
- _('Files'),
- _('Commits'),
+ _('Merge requests'),
+ _('Repository'),
_('Branches'),
+ _('Commits'),
_('Tags'),
- _('Contributor statistics'),
- _('Graph'),
+ _('Repository graph'),
_('Compare revisions'),
+ _('Snippets'),
(_('Locked files') if Gitlab.ee?)
]
},
{
- nav_item: _('Issues'),
- nav_sub_items: [
- _('List'),
- _('Boards'),
- _('Service Desk'),
- _('Milestones')
- ]
- },
- {
- nav_item: _('Merge requests'),
- nav_sub_items: []
- },
- {
- nav_item: _('CI/CD'),
+ nav_item: _('Build'),
nav_sub_items: [
_('Pipelines'),
- s_('Pipelines|Editor'),
_('Jobs'),
- _('Artifacts'),
- _('Schedules')
+ _('Pipeline editor'),
+ _('Pipeline schedules'),
+ _('Artifacts')
]
},
- security_and_compliance_nav_item,
+ secure_nav_item,
{
- nav_item: _('Deployments'),
+ nav_item: _('Deploy'),
nav_sub_items: [
- _('Environments'),
- s_('FeatureFlags|Feature flags'),
- _('Releases')
+ _('Releases'),
+ s_('FeatureFlags|Feature flags')
]
},
{
- nav_item: _('Infrastructure'),
+ nav_item: _('Operate'),
nav_sub_items: [
+ _('Environments'),
_('Kubernetes clusters'),
s_('Terraform|Terraform states')
]
@@ -84,22 +77,15 @@ RSpec.shared_context 'project navbar structure' do
nav_sub_items: [
_('Error Tracking'),
_('Alerts'),
- _('Incidents')
+ _('Incidents'),
+ _('Service Desk')
]
},
{
- nav_item: _('Analytics'),
+ nav_item: _('Analyze'),
nav_sub_items: project_analytics_sub_nav_item
},
{
- nav_item: _('Wiki'),
- nav_sub_items: []
- },
- {
- nav_item: _('Snippets'),
- nav_sub_items: []
- },
- {
nav_item: _('Settings'),
nav_sub_items: [
_('General'),
@@ -120,9 +106,9 @@ RSpec.shared_context 'project navbar structure' do
end
RSpec.shared_context 'group navbar structure' do
- let(:analytics_nav_item) do
+ let(:analyze_nav_item) do
{
- nav_item: _('Analytics'),
+ nav_item: _("Analyze"),
nav_sub_items: group_analytics_sub_nav_item
}
end
@@ -148,65 +134,46 @@ RSpec.shared_context 'group navbar structure' do
let(:settings_for_maintainer_nav_item) do
{
- nav_item: _('Settings'),
- nav_sub_items: [
- _('Repository')
- ]
+ nav_item: _("Settings"),
+ nav_sub_items: [_("Repository")]
}
end
- let(:security_and_compliance_nav_item) do
+ let(:secure_nav_item) do
{
- nav_item: _('Security and Compliance'),
- nav_sub_items: [
- _('Audit events')
- ]
+ nav_item: _("Secure"),
+ nav_sub_items: [_("Audit events")]
}
end
- let(:issues_nav_items) do
- [
- _('List'),
- _('Board'),
- _('Milestones'),
- (_('Iterations') if Gitlab.ee?)
- ]
+ let(:plan_nav_items) do
+ [_("Issues"), _("Issue board"), _("Milestones"), (_('Iterations') if Gitlab.ee?)]
end
let(:structure) do
[
{
- nav_item: "#{group.name[0, 1].upcase} #{group.name}",
- nav_sub_items: []
- },
- {
- nav_item: group.root? ? _('Group information') : _('Subgroup information'),
- nav_sub_items: [
- _('Activity'),
- _('Labels'),
- _('Members')
- ]
+ nav_item: _("Manage"),
+ nav_sub_items: [_("Activity"), _("Members"), _("Labels")]
},
{
- nav_item: _('Issues'),
- nav_sub_items: issues_nav_items
+ nav_item: _("Plan"),
+ nav_sub_items: plan_nav_items
},
{
- nav_item: _('Merge requests'),
- nav_sub_items: []
+ nav_item: _("Code"),
+ nav_sub_items: [_("Merge requests")]
},
- (security_and_compliance_nav_item if Gitlab.ee?),
{
- nav_item: _('CI/CD'),
- nav_sub_items: [
- s_('Runners|Runners')
- ]
+ nav_item: _("Build"),
+ nav_sub_items: [_("Runners")]
},
+ (secure_nav_item if Gitlab.ee?),
{
- nav_item: _('Kubernetes'),
- nav_sub_items: []
+ nav_item: _("Operate"),
+ nav_sub_items: [_("Kubernetes")]
},
- (analytics_nav_item if Gitlab.ee?)
+ (analyze_nav_item if Gitlab.ee?)
]
end
end
@@ -215,10 +182,6 @@ RSpec.shared_context 'dashboard navbar structure' do
let(:structure) do
[
{
- nav_item: "Your work",
- nav_sub_items: []
- },
- {
nav_item: _("Projects"),
nav_sub_items: []
},
@@ -237,8 +200,8 @@ RSpec.shared_context 'dashboard navbar structure' do
{
nav_item: _("Merge requests"),
nav_sub_items: [
- _('Assigned 0'),
- _('Review requests 0')
+ _('Assigned'),
+ _('Review requests')
]
},
{
@@ -265,10 +228,29 @@ RSpec.shared_context '"Explore" navbar structure' do
let(:structure) do
[
{
- nav_item: "Explore",
+ nav_item: _("Projects"),
+ nav_sub_items: []
+ },
+ {
+ nav_item: _("Groups"),
nav_sub_items: []
},
{
+ nav_item: _("Topics"),
+ nav_sub_items: []
+ },
+ {
+ nav_item: _("Snippets"),
+ nav_sub_items: []
+ }
+ ]
+ end
+end
+
+RSpec.shared_context '"Explore" navbar structure with global_ci_catalog FF' do
+ let(:structure) do
+ [
+ {
nav_item: _("Projects"),
nav_sub_items: []
},
@@ -277,6 +259,10 @@ RSpec.shared_context '"Explore" navbar structure' do
nav_sub_items: []
},
{
+ nav_item: _("CI/CD Catalog"),
+ nav_sub_items: []
+ },
+ {
nav_item: _("Topics"),
nav_sub_items: []
},
diff --git a/spec/support/shared_contexts/policies/project_policy_shared_context.rb b/spec/support/shared_contexts/policies/project_policy_shared_context.rb
index 5014a810f35..68eb3539813 100644
--- a/spec/support/shared_contexts/policies/project_policy_shared_context.rb
+++ b/spec/support/shared_contexts/policies/project_policy_shared_context.rb
@@ -54,7 +54,7 @@ RSpec.shared_context 'ProjectPolicy context' do
create_environment create_merge_request_from
admin_metrics_dashboard_annotation create_pipeline create_release
create_wiki destroy_container_image push_code read_pod_logs
- read_terraform_state resolve_note update_build update_commit_status
+ read_terraform_state resolve_note update_build cancel_build update_commit_status
update_container_image update_deployment update_environment
update_merge_request update_pipeline update_release destroy_release
read_resource_group update_resource_group update_escalation_status
diff --git a/spec/support/shared_contexts/services/projects/container_repository/delete_tags_service_shared_context.rb b/spec/support/shared_contexts/services/projects/container_repository/delete_tags_service_shared_context.rb
index 0cf026749ee..01b91cd5db4 100644
--- a/spec/support/shared_contexts/services/projects/container_repository/delete_tags_service_shared_context.rb
+++ b/spec/support/shared_contexts/services/projects/container_repository/delete_tags_service_shared_context.rb
@@ -16,11 +16,11 @@ RSpec.shared_context 'container repository delete tags service shared context' d
stub_container_registry_tags(
repository: repository.path,
- tags: %w(latest A Ba Bb C D E))
+ tags: %w[latest A Ba Bb C D E])
end
def stub_delete_reference_request(tag, status = 200)
- stub_request(:delete, "http://registry.gitlab/v2/#{repository.path}/tags/reference/#{tag}")
+ stub_request(:delete, "http://registry.gitlab/v2/#{repository.path}/manifests/#{tag}")
.to_return(status: status, body: '')
end
@@ -28,7 +28,7 @@ RSpec.shared_context 'container repository delete tags service shared context' d
tags = Array.wrap(tags).index_with { 200 } unless tags.is_a?(Hash)
tags.each do |tag, status|
- stub_request(:delete, "http://registry.gitlab/v2/#{repository.path}/tags/reference/#{tag}")
+ stub_request(:delete, "http://registry.gitlab/v2/#{repository.path}/manifests/#{tag}")
.to_return(status: status, body: '')
end
end
@@ -58,23 +58,11 @@ RSpec.shared_context 'container repository delete tags service shared context' d
.with(repository.path, content, digest) { double(success?: success ) }
end
- def expect_delete_tag_by_digest(digest)
- expect_any_instance_of(ContainerRegistry::Client)
- .to receive(:delete_repository_tag_by_digest)
- .with(repository.path, digest) { true }
-
- expect_any_instance_of(ContainerRegistry::Client)
- .not_to receive(:delete_repository_tag_by_name)
- end
-
- def expect_delete_tag_by_names(names)
+ def expect_delete_tags(names)
Array.wrap(names).each do |name|
expect_any_instance_of(ContainerRegistry::Client)
- .to receive(:delete_repository_tag_by_name)
+ .to receive(:delete_repository_tag_by_digest)
.with(repository.path, name) { true }
-
- expect_any_instance_of(ContainerRegistry::Client)
- .not_to receive(:delete_repository_tag_by_digest)
end
end
end
diff --git a/spec/support/shared_examples/analytics/cycle_analytics/request_params_examples.rb b/spec/support/shared_examples/analytics/cycle_analytics/request_params_examples.rb
index 0e7b909fce9..cf539174587 100644
--- a/spec/support/shared_examples/analytics/cycle_analytics/request_params_examples.rb
+++ b/spec/support/shared_examples/analytics/cycle_analytics/request_params_examples.rb
@@ -45,9 +45,19 @@ RSpec.shared_examples 'unlicensed cycle analytics request params' do
end
end
+ context 'when the date range is exactly 180 days' do
+ before do
+ params[:created_before] = '2019-06-30'
+ end
+
+ it 'is valid' do
+ expect(subject).to be_valid
+ end
+ end
+
context 'when the date range exceeds 180 days' do
before do
- params[:created_before] = '2019-07-15'
+ params[:created_before] = '2019-07-01'
end
it 'is invalid' do
diff --git a/spec/support/shared_examples/boards/multiple_issue_boards_shared_examples.rb b/spec/support/shared_examples/boards/multiple_issue_boards_shared_examples.rb
index c86fcf5ae20..9bd10d56d8c 100644
--- a/spec/support/shared_examples/boards/multiple_issue_boards_shared_examples.rb
+++ b/spec/support/shared_examples/boards/multiple_issue_boards_shared_examples.rb
@@ -5,7 +5,6 @@ RSpec.shared_examples 'multiple issue boards' do
context 'authorized user' do
before do
- stub_feature_flags(apollo_boards: false)
parent.add_maintainer(user)
login_as(user)
@@ -124,7 +123,6 @@ RSpec.shared_examples 'multiple issue boards' do
context 'unauthorized user' do
before do
- stub_feature_flags(apollo_boards: false)
visit boards_path
wait_for_requests
end
@@ -174,6 +172,8 @@ RSpec.shared_examples 'multiple issue boards' do
end
def assert_boards_nav_active
- expect(find('.nav-sidebar .active .active')).to have_selector('a', text: 'Boards')
+ within_testid('super-sidebar') do
+ expect(page).to have_selector('[aria-current="page"]', text: 'Issue boards')
+ end
end
end
diff --git a/spec/support/shared_examples/ci/deployable_policy_shared_examples.rb b/spec/support/shared_examples/ci/deployable_policy_shared_examples.rb
index 73bdc094237..1f164a66026 100644
--- a/spec/support/shared_examples/ci/deployable_policy_shared_examples.rb
+++ b/spec/support/shared_examples/ci/deployable_policy_shared_examples.rb
@@ -20,6 +20,7 @@ RSpec.shared_examples 'a deployable job policy' do |factory_type|
end
it { expect(policy).not_to be_allowed :update_build }
+ it { expect(policy).not_to be_allowed :cancel_build }
end
end
end
diff --git a/spec/support/shared_examples/ci/deployable_policy_shared_examples_ee.rb b/spec/support/shared_examples/ci/deployable_policy_shared_examples_ee.rb
index b1057b3f67a..10f334a6e23 100644
--- a/spec/support/shared_examples/ci/deployable_policy_shared_examples_ee.rb
+++ b/spec/support/shared_examples/ci/deployable_policy_shared_examples_ee.rb
@@ -20,6 +20,12 @@ RSpec.shared_examples 'a deployable job policy in EE' do |factory_type|
it_behaves_like 'protected environments access', direct_access: true
end
+ describe '#cancel_build?' do
+ subject { user.can?(:cancel_build, job) }
+
+ it_behaves_like 'protected environments access', direct_access: true
+ end
+
describe '#update_commit_status?' do
subject { user.can?(:update_commit_status, job) }
diff --git a/spec/support/shared_examples/ci/redis_shared_examples.rb b/spec/support/shared_examples/ci/redis_shared_examples.rb
new file mode 100644
index 00000000000..bc9826a8968
--- /dev/null
+++ b/spec/support/shared_examples/ci/redis_shared_examples.rb
@@ -0,0 +1,222 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'CI build trace chunk redis' do |redis_store|
+ describe '#data' do
+ subject { data_store.data(model) }
+
+ context 'when data exists' do
+ let(:model) { create(:ci_build_trace_chunk, store_trait_with_data, initial_data: 'sample data in redis') }
+
+ it 'returns the data' do
+ is_expected.to eq('sample data in redis')
+ end
+ end
+
+ context 'when data does not exist' do
+ let(:model) { create(:ci_build_trace_chunk, store_trait_without_data) }
+
+ it 'returns nil' do
+ is_expected.to be_nil
+ end
+ end
+ end
+
+ describe '#set_data' do
+ subject { data_store.set_data(model, data) }
+
+ let(:data) { 'abc123' }
+
+ context 'when data exists' do
+ let(:model) { create(:ci_build_trace_chunk, store_trait_with_data, initial_data: 'sample data in redis') }
+
+ it 'overwrites data' do
+ expect(data_store.data(model)).to eq('sample data in redis')
+
+ subject
+
+ expect(data_store.data(model)).to eq('abc123')
+ end
+ end
+
+ context 'when data does not exist' do
+ let(:model) { create(:ci_build_trace_chunk, store_trait_without_data) }
+
+ it 'sets new data' do
+ expect(data_store.data(model)).to be_nil
+
+ subject
+
+ expect(data_store.data(model)).to eq('abc123')
+ end
+ end
+ end
+
+ describe '#append_data' do
+ context 'when valid offset is used with existing data' do
+ let(:model) { create(:ci_build_trace_chunk, store_trait_with_data, initial_data: 'abcd') }
+
+ it 'appends data' do
+ expect(data_store.data(model)).to eq('abcd')
+
+ length = data_store.append_data(model, '12345', 4)
+
+ expect(length).to eq 9
+ expect(data_store.data(model)).to eq('abcd12345')
+ end
+ end
+
+ context 'when data does not exist yet' do
+ let(:model) { create(:ci_build_trace_chunk, store_trait_without_data) }
+
+ it 'sets new data' do
+ expect(data_store.data(model)).to be_nil
+
+ length = data_store.append_data(model, 'abc', 0)
+
+ expect(length).to eq 3
+ expect(data_store.data(model)).to eq('abc')
+ end
+ end
+
+ context 'when data needs to be truncated' do
+ let(:model) { create(:ci_build_trace_chunk, store_trait_with_data, initial_data: '12345678') }
+
+ it 'appends data and truncates stored value' do
+ expect(data_store.data(model)).to eq('12345678')
+
+ length = data_store.append_data(model, 'ab', 4)
+
+ expect(length).to eq 6
+ expect(data_store.data(model)).to eq('1234ab')
+ end
+ end
+
+ context 'when invalid offset is provided' do
+ let(:model) { create(:ci_build_trace_chunk, store_trait_with_data, initial_data: 'abc') }
+
+ it 'raises an exception' do
+ length = data_store.append_data(model, '12345', 4)
+
+ expect(length).to be_negative
+ end
+ end
+
+ context 'when trace contains multi-byte UTF8 characters' do
+ let(:model) { create(:ci_build_trace_chunk, store_trait_with_data, initial_data: 'aüc') }
+
+ it 'appends data' do
+ length = data_store.append_data(model, '1234', 4)
+
+ data_store.data(model).then do |new_data|
+ expect(new_data.bytesize).to eq 8
+ expect(new_data).to eq 'aüc1234'
+ end
+
+ expect(length).to eq 8
+ end
+ end
+
+ context 'when trace contains non-UTF8 characters' do
+ let(:model) { create(:ci_build_trace_chunk, store_trait_with_data, initial_data: "a\255c") }
+
+ it 'appends data' do
+ length = data_store.append_data(model, '1234', 3)
+
+ data_store.data(model).then do |new_data|
+ expect(new_data.bytesize).to eq 7
+ end
+
+ expect(length).to eq 7
+ end
+ end
+ end
+
+ describe '#delete_data' do
+ subject { data_store.delete_data(model) }
+
+ context 'when data exists' do
+ let(:model) { create(:ci_build_trace_chunk, store_trait_with_data, initial_data: 'sample data in redis') }
+
+ it 'deletes data' do
+ expect(data_store.data(model)).to eq('sample data in redis')
+
+ subject
+
+ expect(data_store.data(model)).to be_nil
+ end
+ end
+
+ context 'when data does not exist' do
+ let(:model) { create(:ci_build_trace_chunk, store_trait_without_data) }
+
+ it 'does nothing' do
+ expect(data_store.data(model)).to be_nil
+
+ subject
+
+ expect(data_store.data(model)).to be_nil
+ end
+ end
+ end
+
+ describe '#size' do
+ context 'when data exists' do
+ let(:model) { create(:ci_build_trace_chunk, store_trait_with_data, initial_data: 'üabcd') }
+
+ it 'returns data bytesize correctly' do
+ expect(data_store.size(model)).to eq 6
+ end
+ end
+
+ context 'when data does not exist' do
+ let(:model) { create(:ci_build_trace_chunk, store_trait_without_data) }
+
+ it 'returns zero' do
+ expect(data_store.size(model)).to be_zero
+ end
+ end
+ end
+
+ describe '#keys' do
+ subject { data_store.keys(relation) }
+
+ let(:build) { create(:ci_build) }
+ let(:relation) { build.trace_chunks }
+
+ before do
+ create(:ci_build_trace_chunk, store_trait_with_data, chunk_index: 0, build: build)
+ create(:ci_build_trace_chunk, store_trait_with_data, chunk_index: 1, build: build)
+ end
+
+ it 'returns keys' do
+ is_expected.to eq([[build.id, 0], [build.id, 1]])
+ end
+ end
+
+ describe '#delete_keys' do
+ subject { data_store.delete_keys(keys) }
+
+ let(:build) { create(:ci_build) }
+ let(:relation) { build.trace_chunks }
+ let(:keys) { data_store.keys(relation) }
+
+ before do
+ create(:ci_build_trace_chunk, store_trait_with_data, chunk_index: 0, build: build)
+ create(:ci_build_trace_chunk, store_trait_with_data, chunk_index: 1, build: build)
+ end
+
+ it 'deletes multiple data' do
+ redis_store.with do |redis|
+ expect(redis.exists?("gitlab:ci:trace:#{build.id}:chunks:0")).to eq(true)
+ expect(redis.exists?("gitlab:ci:trace:#{build.id}:chunks:1")).to eq(true)
+ end
+
+ subject
+
+ redis_store.with do |redis|
+ expect(redis.exists?("gitlab:ci:trace:#{build.id}:chunks:0")).to eq(false)
+ expect(redis.exists?("gitlab:ci:trace:#{build.id}:chunks:1")).to eq(false)
+ end
+ end
+ end
+end
diff --git a/spec/support/shared_examples/controllers/base_action_controller_shared_examples.rb b/spec/support/shared_examples/controllers/base_action_controller_shared_examples.rb
deleted file mode 100644
index 5f236f25d35..00000000000
--- a/spec/support/shared_examples/controllers/base_action_controller_shared_examples.rb
+++ /dev/null
@@ -1,26 +0,0 @@
-# frozen_string_literal: true
-
-# Requires `request` subject to be defined
-#
-# subject(:request) { get root_path }
-RSpec.shared_examples 'Base action controller' do
- describe 'security headers' do
- describe 'Cross-Origin-Opener-Policy' do
- it 'sets the header' do
- request
-
- expect(response.headers['Cross-Origin-Opener-Policy']).to eq('same-origin')
- end
-
- context 'when coop_header feature flag is disabled' do
- it 'does not set the header' do
- stub_feature_flags(coop_header: false)
-
- request
-
- expect(response.headers['Cross-Origin-Opener-Policy']).to be_nil
- end
- end
- end
- end
-end
diff --git a/spec/support/shared_examples/controllers/is_ambiguous_ref_examples.rb b/spec/support/shared_examples/controllers/is_ambiguous_ref_examples.rb
new file mode 100644
index 00000000000..24a656514c4
--- /dev/null
+++ b/spec/support/shared_examples/controllers/is_ambiguous_ref_examples.rb
@@ -0,0 +1,55 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples '#set_is_ambiguous_ref when ref is ambiguous' do
+ context 'when the ref_type is nil' do
+ let(:ref_type) { nil }
+
+ it '@ambiguous_ref return false when ff is disabled' do
+ expect(controller.instance_variable_get(:@is_ambiguous_ref)).to eq(false)
+ end
+
+ context 'when the ambiguous_ref_modal ff is enabled' do
+ let(:ambiguous_ref_modal) { true }
+
+ it '@ambiguous_ref return true' do
+ expect(controller.instance_variable_get(:@is_ambiguous_ref)).to eq(true)
+ end
+ end
+ end
+
+ context 'when the ref_type is empty' do
+ let(:ref_type) { '' }
+
+ it '@ambiguous_ref return false when ff is disabled' do
+ expect(controller.instance_variable_get(:@is_ambiguous_ref)).to eq(false)
+ end
+
+ context 'when the ambiguous_ref_modal ff is enabled' do
+ let(:ambiguous_ref_modal) { true }
+
+ it '@ambiguous_ref return true' do
+ expect(controller.instance_variable_get(:@is_ambiguous_ref)).to eq(true)
+ end
+ end
+ end
+
+ context 'when the ref_type is present' do
+ let(:ref_type) { 'heads' }
+ let(:ambiguous_ref_modal) { true }
+
+ it '@ambiguous_ref return false' do
+ expect(controller.instance_variable_get(:@is_ambiguous_ref)).to eq(false)
+ end
+ end
+end
+
+RSpec.shared_examples '#set_is_ambiguous_ref when ref is not ambiguous' do
+ context 'when the ref_type is nil' do
+ let(:ref_type) { nil }
+ let(:ambiguous_ref_modal) { true }
+
+ it '@ambiguous_ref return false' do
+ expect(controller.instance_variable_get(:@is_ambiguous_ref)).to eq(false)
+ end
+ end
+end
diff --git a/spec/support/shared_examples/controllers/wiki_actions_shared_examples.rb b/spec/support/shared_examples/controllers/wiki_actions_shared_examples.rb
index 32aa566c27e..8cec0cdfbf5 100644
--- a/spec/support/shared_examples/controllers/wiki_actions_shared_examples.rb
+++ b/spec/support/shared_examples/controllers/wiki_actions_shared_examples.rb
@@ -324,7 +324,7 @@ RSpec.shared_examples 'wiki controller actions' do
post :preview_markdown, params: routing_params.merge(id: 'page/path', text: '*Markdown* text')
expect(response).to have_gitlab_http_status(:ok)
- expect(json_response.keys).to match_array(%w(body references))
+ expect(json_response.keys).to match_array(%w[body references])
end
end
diff --git a/spec/support/shared_examples/database_health_status_indicators/prometheus_alert_based_shared_examples.rb b/spec/support/shared_examples/database_health_status_indicators/prometheus_alert_based_shared_examples.rb
index 109a349a652..ddc438cb652 100644
--- a/spec/support/shared_examples/database_health_status_indicators/prometheus_alert_based_shared_examples.rb
+++ b/spec/support/shared_examples/database_health_status_indicators/prometheus_alert_based_shared_examples.rb
@@ -2,7 +2,7 @@
RSpec.shared_examples 'Prometheus Alert based health indicator' do
let(:schema) { :main }
- let(:connection) { Gitlab::Database.database_base_models[schema].connection }
+ let(:connection) { Gitlab::Database.database_base_models_with_gitlab_shared[schema].connection }
around do |example|
Gitlab::Database::SharedModel.using_connection(connection) do
@@ -124,7 +124,7 @@ RSpec.shared_examples 'Prometheus Alert based health indicator' do
end
end
- Gitlab::Database.database_base_models.each do |database_base_model, connection|
+ Gitlab::Database.database_base_models_with_gitlab_shared.each do |database_base_model, connection|
next unless connection.present?
it_behaves_like 'Patroni Apdex Evaluator', database_base_model.to_sym
diff --git a/spec/support/shared_examples/features/2fa_shared_examples.rb b/spec/support/shared_examples/features/2fa_shared_examples.rb
index f50874b6b05..8744259488f 100644
--- a/spec/support/shared_examples/features/2fa_shared_examples.rb
+++ b/spec/support/shared_examples/features/2fa_shared_examples.rb
@@ -14,7 +14,7 @@ RSpec.shared_examples 'hardware device for 2fa' do |device_type|
end
describe "registration" do
- let(:user) { create(:user, :no_super_sidebar) }
+ let(:user) { create(:user) }
before do
gitlab_sign_in(user)
@@ -66,8 +66,8 @@ RSpec.shared_examples 'hardware device for 2fa' do |device_type|
end
end
- describe 'fallback code authentication' do
- let(:user) { create(:user, :no_super_sidebar) }
+ describe 'fallback code authentication', :js do
+ let(:user) { create(:user) }
before do
# Register and logout
diff --git a/spec/support/shared_examples/features/content_editor_shared_examples.rb b/spec/support/shared_examples/features/content_editor_shared_examples.rb
index 3e81f969462..6bfb60c3f34 100644
--- a/spec/support/shared_examples/features/content_editor_shared_examples.rb
+++ b/spec/support/shared_examples/features/content_editor_shared_examples.rb
@@ -2,7 +2,10 @@
require 'spec_helper'
-RSpec.shared_examples 'edits content using the content editor' do |params = { with_expanded_references: true }|
+RSpec.shared_examples 'edits content using the content editor' do |params = {
+ with_expanded_references: true,
+ with_quick_actions: true
+}|
include ContentEditorHelpers
let(:content_editor_testid) { '[data-testid="content-editor"] [contenteditable].ProseMirror' }
@@ -463,6 +466,15 @@ RSpec.shared_examples 'edits content using the content editor' do |params = { wi
end
end
+ it 'does not show a loading indicator after undo paste' do
+ type_in_content_editor [modifier_key, 'v']
+ type_in_content_editor [modifier_key, 'z']
+
+ page.within content_editor_testid do
+ expect(page).not_to have_css('.gl-dots-loader')
+ end
+ end
+
it 'pastes raw text without formatting if shift + ctrl + v is pressed' do
type_in_content_editor [modifier_key, :shift, 'v']
@@ -546,6 +558,42 @@ RSpec.shared_examples 'edits content using the content editor' do |params = { wi
end
end
+ if params[:with_quick_actions]
+ it 'shows suggestions for quick actions' do
+ type_in_content_editor '/a'
+
+ expect(find(suggestions_dropdown)).to have_text('/assign')
+ expect(find(suggestions_dropdown)).to have_text('/label')
+ end
+
+ it 'adds the correct prefix for /assign' do
+ type_in_content_editor '/assign'
+
+ expect(find(suggestions_dropdown)).to have_text('/assign')
+ send_keys [:arrow_down, :enter]
+
+ expect(page).to have_text('/assign @')
+ end
+
+ it 'adds the correct prefix for /label' do
+ type_in_content_editor '/label'
+
+ expect(find(suggestions_dropdown)).to have_text('/label')
+ send_keys [:arrow_down, :enter]
+
+ expect(page).to have_text('/label ~')
+ end
+
+ it 'adds the correct prefix for /milestone' do
+ type_in_content_editor '/milestone'
+
+ expect(find(suggestions_dropdown)).to have_text('/milestone')
+ send_keys [:arrow_down, :enter]
+
+ expect(page).to have_text('/milestone %')
+ end
+ end
+
it 'shows suggestions for members with descriptions' do
type_in_content_editor '@a'
@@ -561,6 +609,39 @@ RSpec.shared_examples 'edits content using the content editor' do |params = { wi
expect(page).to have_text('@abc123')
end
+ it 'allows dismissing the suggestion popup and typing more text' do
+ type_in_content_editor '@ab'
+
+ expect(find(suggestions_dropdown)).to have_text('abc123')
+
+ send_keys :escape
+
+ expect(page).not_to have_css(suggestions_dropdown)
+
+ type_in_content_editor :enter
+ type_in_content_editor 'foobar'
+
+ # ensure that the texts are in separate paragraphs
+ expect(page).to have_selector('p', text: '@ab')
+ expect(page).to have_selector('p', text: 'foobar')
+ expect(page).not_to have_selector('p', text: '@abfoobar')
+ end
+
+ it 'allows typing more text after the popup has disappeared because no suggestions match' do
+ type_in_content_editor '@ab'
+
+ expect(find(suggestions_dropdown)).to have_text('abc123')
+
+ type_in_content_editor 'foo'
+ type_in_content_editor :enter
+ type_in_content_editor 'bar'
+
+ # ensure that the texts are in separate paragraphs
+ expect(page).to have_selector('p', text: '@abfoo')
+ expect(page).to have_selector('p', text: 'bar')
+ expect(page).not_to have_selector('p', text: '@abfoobar')
+ end
+
context 'when `disable_all_mention` is enabled' do
before do
stub_feature_flags(disable_all_mention: true)
@@ -617,14 +698,14 @@ RSpec.shared_examples 'edits content using the content editor' do |params = { wi
it 'shows suggestions for emojis' do
type_in_content_editor ':smile'
- expect(find(suggestions_dropdown)).to have_text('🙂 slight_smile')
+ expect(find(suggestions_dropdown)).to have_text('😃 smiley')
expect(find(suggestions_dropdown)).to have_text('😸 smile_cat')
send_keys [:arrow_down, :enter]
expect(page).not_to have_css(suggestions_dropdown)
- expect(page).to have_text('🙂')
+ expect(page).to have_text('😃')
end
it 'doesn\'t show suggestions dropdown if there are no suggestions to show' do
@@ -640,7 +721,7 @@ RSpec.shared_examples 'edits content using the content editor' do |params = { wi
it 'scrolls selected item into view when navigating with keyboard' do
type_in_content_editor ':'
- expect(find(suggestions_dropdown)).to have_text('hundred points symbol')
+ expect(find(suggestions_dropdown)).to have_text('grinning')
expect(dropdown_scroll_top).to be 0
diff --git a/spec/support/shared_examples/features/dashboard/sidebar_shared_examples.rb b/spec/support/shared_examples/features/dashboard/sidebar_shared_examples.rb
index 9b5d9d66890..0dbf186e9b3 100644
--- a/spec/support/shared_examples/features/dashboard/sidebar_shared_examples.rb
+++ b/spec/support/shared_examples/features/dashboard/sidebar_shared_examples.rb
@@ -6,23 +6,19 @@ RSpec.shared_examples 'a "Your work" page with sidebar and breadcrumbs' do |page
visit send(page_path)
end
- let(:sidebar_css) { "aside.nav-sidebar[aria-label=\"Your work\"]" }
- let(:active_menu_item_css) { "li.active[data-track-label=\"#{menu_label}_menu\"]" }
-
it "shows the \"Your work\" sidebar" do
- expect(page).to have_css(sidebar_css)
+ expect(page).to have_css('#super-sidebar-context-header', text: 'Your work')
end
it "shows the correct sidebar menu item as active" do
- within(sidebar_css) do
- expect(page).to have_css(active_menu_item_css)
+ within_testid('super-sidebar') do
+ expect(page).to have_css("a[data-track-label='#{menu_label}_menu'][aria-current='page']")
end
end
describe "breadcrumbs" do
it 'has "Your work" as its root breadcrumb' do
- breadcrumbs = page.find('[data-testid="breadcrumb-links"]')
- within breadcrumbs do
+ within_testid('breadcrumb-links') do
expect(page).to have_css("li:first-child a[href=\"#{root_path}\"]", text: "Your work")
end
end
diff --git a/spec/support/shared_examples/features/explore/sidebar_shared_examples.rb b/spec/support/shared_examples/features/explore/sidebar_shared_examples.rb
deleted file mode 100644
index 1754c8bf53d..00000000000
--- a/spec/support/shared_examples/features/explore/sidebar_shared_examples.rb
+++ /dev/null
@@ -1,28 +0,0 @@
-# frozen_string_literal: true
-
-RSpec.shared_examples 'an "Explore" page with sidebar and breadcrumbs' do |page_path, menu_label|
- before do
- visit send(page_path)
- end
-
- let(:sidebar_css) { 'aside.nav-sidebar[aria-label="Explore"]' }
- let(:active_menu_item_css) { "li.active[data-track-label=\"#{menu_label}_menu\"]" }
-
- it 'shows the "Explore" sidebar' do
- expect(page).to have_css(sidebar_css)
- end
-
- it 'shows the correct sidebar menu item as active' do
- within(sidebar_css) do
- expect(page).to have_css(active_menu_item_css)
- end
- end
-
- describe 'breadcrumbs' do
- it 'has "Explore" as its root breadcrumb' do
- within '.breadcrumbs-list' do
- expect(page).to have_css("li:first a[href=\"#{explore_root_path}\"]", text: 'Explore')
- end
- end
- end
-end
diff --git a/spec/support/shared_examples/features/issuable_invite_members_shared_examples.rb b/spec/support/shared_examples/features/issuable_invite_members_shared_examples.rb
index b8c6b85adb2..f53aaa4514d 100644
--- a/spec/support/shared_examples/features/issuable_invite_members_shared_examples.rb
+++ b/spec/support/shared_examples/features/issuable_invite_members_shared_examples.rb
@@ -4,22 +4,17 @@ RSpec.shared_examples 'issuable invite members' do
include Features::InviteMembersModalHelpers
context 'when a privileged user can invite' do
- before do
- project.add_maintainer(user)
- end
-
it 'shows a link for inviting members and launches invite modal' do
+ project.add_maintainer(user)
visit issuable_path
- find('.block.assignee .edit-link').click
-
- wait_for_requests
+ open_assignees_dropdown
page.within '.dropdown-menu-user' do
- expect(page).to have_link('Invite Members')
- end
+ expect(page).to have_link('Invite members')
- click_link 'Invite Members'
+ click_link 'Invite members'
+ end
page.within invite_modal_selector do
expect(page).to have_content("You're inviting members to the #{project.name} project")
@@ -28,19 +23,14 @@ RSpec.shared_examples 'issuable invite members' do
end
context 'when user cannot invite members in assignee dropdown' do
- before do
- project.add_developer(user)
- end
-
it 'shows author in assignee dropdown and no invite link' do
+ project.add_developer(user)
visit issuable_path
- find('.block.assignee .edit-link').click
-
- wait_for_requests
+ open_assignees_dropdown
page.within '.dropdown-menu-user' do
- expect(page).not_to have_link('Invite Members')
+ expect(page).not_to have_link('Invite members')
end
end
end
diff --git a/spec/support/shared_examples/features/nav_sidebar_shared_examples.rb b/spec/support/shared_examples/features/nav_sidebar_shared_examples.rb
index 34821fb9eda..8c668ae9a87 100644
--- a/spec/support/shared_examples/features/nav_sidebar_shared_examples.rb
+++ b/spec/support/shared_examples/features/nav_sidebar_shared_examples.rb
@@ -2,15 +2,16 @@
RSpec.shared_examples 'page has active tab' do |title|
it "activates #{title} tab" do
- expect(page).to have_selector('.sidebar-top-level-items > li.active', count: 1)
- expect(find('.sidebar-top-level-items > li.active')).to have_content(title)
+ within_testid('super-sidebar') do
+ expect(page).to have_selector('button[aria-expanded="true"]', text: title)
+ end
end
end
RSpec.shared_examples 'page has active sub tab' do |title|
it "activates #{title} sub tab" do
- expect(page).to have_selector('.sidebar-sub-level-items > li.active:not(.fly-out-top-item)', count: 1)
- expect(find('.sidebar-sub-level-items > li.active:not(.fly-out-top-item)'))
- .to have_content(title)
+ within_testid('super-sidebar') do
+ expect(page).to have_selector('a[aria-current="page"]', text: title)
+ end
end
end
diff --git a/spec/support/shared_examples/features/navbar_shared_examples.rb b/spec/support/shared_examples/features/navbar_shared_examples.rb
index 9b89a3b5e54..af601c2b8e5 100644
--- a/spec/support/shared_examples/features/navbar_shared_examples.rb
+++ b/spec/support/shared_examples/features/navbar_shared_examples.rb
@@ -8,17 +8,12 @@ RSpec.shared_examples 'verified navigation bar' do
end
it 'renders correctly' do
- # we are using * here in the selectors to prevent a regression where we added a non 'li' inside an 'ul'
- current_structure = page.all('.sidebar-top-level-items > *', class: ['!hidden']).map do |item|
- next if item.find_all('a').empty?
-
- nav_item = item.find_all('a').first.text.gsub(/\s+\d+$/, '') # remove counts at the end
-
- nav_sub_items = item.all('.sidebar-sub-level-items > *', class: ['!fly-out-top-item']).map do |list_item|
- list_item.all('a').first.text
+ current_structure = page.all('[data-testid="non-static-items-section"] > li').map do |item|
+ nav_sub_items = item.all('li', visible: :all).map do |list_item|
+ list_item.all('a', visible: :all).first.text(:all).gsub(/\s+\d+$/, '') # remove counts at the end
end
- { nav_item: nav_item, nav_sub_items: nav_sub_items }
+ { nav_item: item.text, nav_sub_items: nav_sub_items }
end.compact
expect(current_structure).to eq(expected_structure)
diff --git a/spec/support/shared_examples/features/packages_shared_examples.rb b/spec/support/shared_examples/features/packages_shared_examples.rb
index 8e8e7e8ad05..6d283113e85 100644
--- a/spec/support/shared_examples/features/packages_shared_examples.rb
+++ b/spec/support/shared_examples/features/packages_shared_examples.rb
@@ -158,7 +158,7 @@ def click_sort_option(option, ascending)
wait_for_requests
# Reset the sort direction
- if page.has_selector?('button[aria-label="Sorting Direction: Ascending"]', wait: 0) && !ascending
+ if page.has_selector?('button[aria-label="Sort direction: Ascending"]', wait: 0) && !ascending
click_button 'Sort direction'
wait_for_requests
diff --git a/spec/support/shared_examples/features/page_description_shared_examples.rb b/spec/support/shared_examples/features/page_description_shared_examples.rb
index e3ea36633d1..163c65915ba 100644
--- a/spec/support/shared_examples/features/page_description_shared_examples.rb
+++ b/spec/support/shared_examples/features/page_description_shared_examples.rb
@@ -2,7 +2,7 @@
RSpec.shared_examples 'page meta description' do |expected_description|
it 'renders the page with description, og:description, and twitter:description meta tags that contains a plain-text version of the markdown', :aggregate_failures do
- %w(name='description' property='og:description' property='twitter:description').each do |selector|
+ %w[name='description' property='og:description' property='twitter:description'].each do |selector|
expect(page).to have_selector("meta[#{selector}][content='#{expected_description}']", visible: false)
end
end
@@ -12,7 +12,7 @@ RSpec.shared_examples 'default brand title page meta description' do
include AppearancesHelper
it 'renders the page with description, og:description, and twitter:description meta tags with the default brand title', :aggregate_failures do
- %w(name='description' property='og:description' property='twitter:description').each do |selector|
+ %w[name='description' property='og:description' property='twitter:description'].each do |selector|
expect(page).to have_selector("meta[#{selector}][content='#{default_brand_title}']", visible: false)
end
end
diff --git a/spec/support/shared_examples/features/project_features_apply_to_issuables_shared_examples.rb b/spec/support/shared_examples/features/project_features_apply_to_issuables_shared_examples.rb
index 58bf461c733..d410653ca43 100644
--- a/spec/support/shared_examples/features/project_features_apply_to_issuables_shared_examples.rb
+++ b/spec/support/shared_examples/features/project_features_apply_to_issuables_shared_examples.rb
@@ -4,8 +4,8 @@ RSpec.shared_examples 'project features apply to issuables' do |klass|
let(:described_class) { klass }
let(:group) { create(:group) }
- let(:user_in_group) { create(:group_member, :developer, user: create(:user, :no_super_sidebar), group: group ).user }
- let(:user_outside_group) { create(:user, :no_super_sidebar) }
+ let(:user_in_group) { create(:group_member, :developer, user: create(:user), group: group ).user }
+ let(:user_outside_group) { create(:user) }
let(:project) { create(:project, :public, project_args) }
diff --git a/spec/support/shared_examples/features/search/search_timeouts_shared_examples.rb b/spec/support/shared_examples/features/search/search_timeouts_shared_examples.rb
index b73f40ff28c..1e7f75d2ac0 100644
--- a/spec/support/shared_examples/features/search/search_timeouts_shared_examples.rb
+++ b/spec/support/shared_examples/features/search/search_timeouts_shared_examples.rb
@@ -17,7 +17,7 @@ RSpec.shared_examples 'search timeouts' do |scope|
end
it 'sets tab count to 0' do
- expect(page.find('[data-testid="search-filter"] .active')).to have_text('0')
+ expect(page.find('[data-testid="search-filter"] [aria-current="page"]')).to have_text('0')
end
end
end
diff --git a/spec/support/shared_examples/features/snippets_shared_examples.rb b/spec/support/shared_examples/features/snippets_shared_examples.rb
index 383f81d048f..0f830fa125a 100644
--- a/spec/support/shared_examples/features/snippets_shared_examples.rb
+++ b/spec/support/shared_examples/features/snippets_shared_examples.rb
@@ -52,30 +52,24 @@ RSpec.shared_examples 'tabs with counts' do
end
RSpec.shared_examples 'does not show New Snippet button' do
- let(:user) { create(:user, :external, :no_super_sidebar) }
-
specify do
- sign_in(user)
-
- subject
-
- wait_for_requests
-
+ expect(page).to have_link(text: "$#{snippet.id}")
expect(page).not_to have_link('New snippet')
end
end
-RSpec.shared_examples 'show and render proper snippet blob' do
- before do
- allow_any_instance_of(Snippet).to receive(:blobs).and_return([snippet.repository.blob_at('master', file_path)])
+RSpec.shared_examples 'does show New Snippet button' do
+ specify do
+ expect(page).to have_link(text: "$#{snippet.id}")
+ expect(page).to have_link('New snippet')
end
+end
+RSpec.shared_examples 'show and render proper snippet blob' do
context 'Ruby file' do
let(:file_path) { 'files/ruby/popen.rb' }
it 'displays the blob' do
- subject
-
aggregate_failures do
# shows highlighted Ruby code
expect(page).to have_content("require 'fileutils'")
@@ -99,10 +93,6 @@ RSpec.shared_examples 'show and render proper snippet blob' do
let(:file_path) { 'files/markdown/ruby-style-guide.md' }
context 'visiting directly' do
- before do
- subject
- end
-
it 'displays the blob using the rich viewer' do
aggregate_failures do
# hides the simple viewer
@@ -171,8 +161,6 @@ RSpec.shared_examples 'show and render proper snippet blob' do
let(:anchor) { 'LC1' }
it 'displays the blob using the simple viewer' do
- subject
-
aggregate_failures do
# hides the rich viewer
expect(page).to have_selector('.blob-viewer[data-type="simple"]')
diff --git a/spec/support/shared_examples/features/variable_list_env_scope_shared_examples.rb b/spec/support/shared_examples/features/variable_list_env_scope_shared_examples.rb
new file mode 100644
index 00000000000..c40d70b85d3
--- /dev/null
+++ b/spec/support/shared_examples/features/variable_list_env_scope_shared_examples.rb
@@ -0,0 +1,87 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'variable list env scope' do
+ include ListboxHelpers
+
+ let(:user) { create(:user) }
+ let(:project) { build(:project) }
+ let(:page_path) { project_settings_ci_cd_path(project) }
+
+ before do
+ sign_in(user)
+ project.add_maintainer(user)
+
+ visit page_path
+ wait_for_requests
+ end
+
+ it 'adds a new variable with an environment scope' do
+ open_drawer
+
+ page.within('[data-testid="ci-variable-drawer"]') do
+ fill_in 'Key', with: 'akey'
+ fill_in 'Value', with: 'akey_value'
+
+ click_button('All (default)')
+ fill_in 'Search', with: 'review/*'
+ find('[data-testid="create-wildcard-button"]').click
+
+ click_button('Add variable')
+ end
+
+ wait_for_requests
+
+ page.within('[data-testid="ci-variable-table"]') do
+ expect(find('.js-ci-variable-row:first-child [data-label="Environments"]').text).to eq('review/*')
+ end
+ end
+
+ it 'resets environment scope list after closing the form' do
+ project.environments.create!(name: 'dev')
+ project.environments.create!(name: 'env_1')
+ project.environments.create!(name: 'env_2')
+
+ open_drawer
+
+ page.within('[data-testid="ci-variable-drawer"]') do
+ click_button('All (default)')
+
+ # default list of env scopes
+ expect_env_scope_items(['*', 'dev', 'env_1', 'env_2'])
+
+ fill_in 'Search', with: 'env'
+ sleep 0.5 # wait for debounce
+ wait_for_requests
+
+ # search filters the list of env scopes
+ expect_env_scope_items(%w[env_1 env_2])
+
+ find('.gl-drawer-close-button').click
+ end
+
+ # Re-open drawer
+ open_drawer
+
+ page.within('[data-testid="ci-variable-drawer"]') do
+ click_button('All (default)')
+
+ # dropdown should reset back to default list of env scopes
+ expect_env_scope_items(['*', 'dev', 'env_1', 'env_2'])
+ end
+ end
+
+ private
+
+ def open_drawer
+ page.within('[data-testid="ci-variable-table"]') do
+ click_button('Add variable')
+ wait_for_requests
+ end
+ end
+
+ def expect_env_scope_items(items)
+ page.within('[data-testid="environment-scope"]') do
+ expect_listbox_items(items)
+ end
+ end
+end
diff --git a/spec/support/shared_examples/features/variable_list_shared_examples.rb b/spec/support/shared_examples/features/variable_list_shared_examples.rb
deleted file mode 100644
index 5951d3e781b..00000000000
--- a/spec/support/shared_examples/features/variable_list_shared_examples.rb
+++ /dev/null
@@ -1,292 +0,0 @@
-# frozen_string_literal: true
-
-RSpec.shared_examples 'variable list' do
- it 'shows a list of variables' do
- page.within('[data-testid="ci-variable-table"]') do
- expect(find('.js-ci-variable-row:nth-child(1) td[data-label="Key"]')).to have_content(variable.key)
- end
- end
-
- it 'adds a new CI variable' do
- click_button('Add variable')
-
- fill_variable('key', 'key_value') do
- click_button('Add variable')
- end
-
- wait_for_requests
-
- page.within('[data-testid="ci-variable-table"]') do
- expect(find('.js-ci-variable-row:nth-child(1) td[data-label="Key"]')).to have_content('key')
- end
- end
-
- it 'adds a new protected variable' do
- click_button('Add variable')
-
- fill_variable('key', 'key_value') do
- click_button('Add variable')
- end
-
- wait_for_requests
-
- page.within('[data-testid="ci-variable-table"]') do
- expect(find(".js-ci-variable-row:nth-child(1) td[data-label='#{s_('CiVariables|Key')}']")).to have_content('key')
- expect(find(".js-ci-variable-row:nth-child(1) td[data-label='#{s_('CiVariables|Key')}']")).to have_content(s_('CiVariables|Protected'))
- end
- end
-
- it 'defaults to unmasked' do
- click_button('Add variable')
-
- fill_variable('key', 'key_value') do
- click_button('Add variable')
- end
-
- wait_for_requests
-
- page.within('[data-testid="ci-variable-table"]') do
- expect(find(".js-ci-variable-row:nth-child(1) td[data-label='#{s_('CiVariables|Key')}']")).to have_content('key')
- expect(find(".js-ci-variable-row:nth-child(1) td[data-label='#{s_('CiVariables|Key')}']")).not_to have_content(s_('CiVariables|Masked'))
- end
- end
-
- it 'reveals and hides variables' do
- page.within('[data-testid="ci-variable-table"]') do
- expect(first('.js-ci-variable-row td[data-label="Key"]')).to have_content(variable.key)
- expect(page).to have_content('*' * 5)
-
- click_button('Reveal value')
-
- expect(first('.js-ci-variable-row td[data-label="Key"]')).to have_content(variable.key)
- expect(first('.js-ci-variable-row td[data-label="Value"]')).to have_content(variable.value)
- expect(page).not_to have_content('*' * 5)
-
- click_button('Hide value')
-
- expect(first('.js-ci-variable-row td[data-label="Key"]')).to have_content(variable.key)
- expect(page).to have_content('*' * 5)
- end
- end
-
- it 'deletes a variable' do
- expect(page).to have_selector('.js-ci-variable-row', count: 1)
-
- page.within('[data-testid="ci-variable-table"]') do
- click_button('Edit')
- end
-
- page.within('#add-ci-variable') do
- click_button('Delete variable')
- end
-
- wait_for_requests
-
- expect(first('.js-ci-variable-row').text).to eq('There are no variables yet.')
- end
-
- it 'edits a variable' do
- page.within('[data-testid="ci-variable-table"]') do
- click_button('Edit')
- end
-
- page.within('#add-ci-variable') do
- find('[data-testid="pipeline-form-ci-variable-key"] input').set('new_key')
-
- click_button('Update variable')
- end
-
- wait_for_requests
-
- expect(first('.js-ci-variable-row td[data-label="Key"]')).to have_content('new_key')
- end
-
- it 'edits a variable to be unmasked' do
- page.within('[data-testid="ci-variable-table"]') do
- click_button('Edit')
- end
-
- page.within('#add-ci-variable') do
- find('[data-testid="ci-variable-protected-checkbox"]').click
- find('[data-testid="ci-variable-masked-checkbox"]').click
-
- click_button('Update variable')
- end
-
- wait_for_requests
-
- page.within('[data-testid="ci-variable-table"]') do
- expect(find(".js-ci-variable-row:nth-child(1) td[data-label='#{s_('CiVariables|Key')}']")).to have_content(s_('CiVariables|Protected'))
- expect(find(".js-ci-variable-row:nth-child(1) td[data-label='#{s_('CiVariables|Key')}']")).not_to have_content(s_('CiVariables|Masked'))
- end
- end
-
- it 'edits a variable to be masked' do
- page.within('[data-testid="ci-variable-table"]') do
- click_button('Edit')
- end
-
- page.within('#add-ci-variable') do
- find('[data-testid="ci-variable-masked-checkbox"]').click
-
- click_button('Update variable')
- end
-
- wait_for_requests
-
- page.within('[data-testid="ci-variable-table"]') do
- click_button('Edit')
- end
-
- page.within('#add-ci-variable') do
- find('[data-testid="ci-variable-masked-checkbox"]').click
-
- click_button('Update variable')
- end
-
- page.within('[data-testid="ci-variable-table"]') do
- expect(find(".js-ci-variable-row:nth-child(1) td[data-label='#{s_('CiVariables|Key')}']")).to have_content(s_('CiVariables|Masked'))
- end
- end
-
- it 'shows a validation error box about duplicate keys' do
- click_button('Add variable')
-
- fill_variable('key', 'key_value') do
- click_button('Add variable')
- end
-
- wait_for_requests
-
- click_button('Add variable')
-
- fill_variable('key', 'key_value') do
- click_button('Add variable')
- end
-
- wait_for_requests
-
- expect(find('.flash-container')).to be_present
- expect(find('[data-testid="alert-danger"]').text).to have_content('(key) has already been taken')
- end
-
- it 'allows variable to be added even if no value is provided' do
- click_button('Add variable')
-
- page.within('#add-ci-variable') do
- find('[data-testid="pipeline-form-ci-variable-key"] input').set('empty_mask_key')
-
- expect(find_button('Add variable', disabled: false)).to be_present
- end
- end
-
- it 'shows validation error box about unmaskable values' do
- click_button('Add variable')
-
- fill_variable('empty_mask_key', '???', protected: true, masked: true) do
- expect(page).to have_content('This variable value does not meet the masking requirements.')
- expect(find_button('Add variable', disabled: true)).to be_present
- end
- end
-
- it 'handles multiple edits and a deletion' do
- # Create two variables
- click_button('Add variable')
-
- fill_variable('akey', 'akeyvalue') do
- click_button('Add variable')
- end
-
- wait_for_requests
-
- click_button('Add variable')
-
- fill_variable('zkey', 'zkeyvalue') do
- click_button('Add variable')
- end
-
- wait_for_requests
-
- expect(page).to have_selector('.js-ci-variable-row', count: 3)
-
- # Remove the `akey` variable
- page.within('[data-testid="ci-variable-table"]') do
- page.within('.js-ci-variable-row:first-child') do
- click_button('Edit')
- end
- end
-
- page.within('#add-ci-variable') do
- click_button('Delete variable')
- end
-
- wait_for_requests
-
- # Add another variable
- click_button('Add variable')
-
- fill_variable('ckey', 'ckeyvalue') do
- click_button('Add variable')
- end
-
- wait_for_requests
-
- # expect to find 3 rows of variables in alphabetical order
- expect(page).to have_selector('.js-ci-variable-row', count: 3)
- rows = all('.js-ci-variable-row')
- expect(rows[0].find('td[data-label="Key"]')).to have_content('ckey')
- expect(rows[1].find('td[data-label="Key"]')).to have_content('test_key')
- expect(rows[2].find('td[data-label="Key"]')).to have_content('zkey')
- end
-
- context 'defaults to the application setting' do
- context 'application setting is true' do
- before do
- stub_application_setting(protected_ci_variables: true)
-
- visit page_path
- end
-
- it 'defaults to protected' do
- click_button('Add variable')
-
- page.within('#add-ci-variable') do
- expect(find('[data-testid="ci-variable-protected-checkbox"]')).to be_checked
- end
- end
- end
-
- context 'application setting is false' do
- before do
- stub_application_setting(protected_ci_variables: false)
-
- visit page_path
- end
-
- it 'defaults to unprotected' do
- click_button('Add variable')
-
- page.within('#add-ci-variable') do
- expect(find('[data-testid="ci-variable-protected-checkbox"]')).not_to be_checked
- end
- end
-
- it 'does not show a message regarding the default' do
- expect(page).not_to have_content 'Environment variables are configured by your administrator to be protected by default'
- end
- end
- end
-
- def fill_variable(key, value, protected: false, masked: false)
- wait_for_requests
-
- page.within('#add-ci-variable') do
- find('[data-testid="pipeline-form-ci-variable-key"] input').set(key)
- find('[data-testid="pipeline-form-ci-variable-value"]').set(value) if value.present?
- find('[data-testid="ci-variable-protected-checkbox"]').click if protected
- find('[data-testid="ci-variable-masked-checkbox"]').click if masked
-
- yield
- end
- end
-end
diff --git a/spec/support/shared_examples/features/wiki/user_creates_wiki_page_shared_examples.rb b/spec/support/shared_examples/features/wiki/user_creates_wiki_page_shared_examples.rb
index ed885d7a226..dfad11f3170 100644
--- a/spec/support/shared_examples/features/wiki/user_creates_wiki_page_shared_examples.rb
+++ b/spec/support/shared_examples/features/wiki/user_creates_wiki_page_shared_examples.rb
@@ -56,7 +56,7 @@ RSpec.shared_examples 'User creates wiki page' do
click_on("Create page")
end
- expect(page).to have_current_path(%r(one/two/three-test), ignore_query: true)
+ expect(page).to have_current_path(%r{one/two/three-test}, ignore_query: true)
expect(page).to have_link(href: wiki_page_path(wiki, 'one/two/three-test'))
end
diff --git a/spec/support/shared_examples/features/wiki/user_updates_wiki_page_shared_examples.rb b/spec/support/shared_examples/features/wiki/user_updates_wiki_page_shared_examples.rb
index 784de102f4f..a48ff8a5f77 100644
--- a/spec/support/shared_examples/features/wiki/user_updates_wiki_page_shared_examples.rb
+++ b/spec/support/shared_examples/features/wiki/user_updates_wiki_page_shared_examples.rb
@@ -40,7 +40,7 @@ RSpec.shared_examples 'User updates wiki page' do
click_on('Create page')
end
- expect(page).to have_current_path(%r(one/two/three-test), ignore_query: true)
+ expect(page).to have_current_path(%r{one/two/three-test}, ignore_query: true)
expect(find('.wiki-pages')).to have_content('three')
first(:link, text: 'three').click
@@ -49,7 +49,7 @@ RSpec.shared_examples 'User updates wiki page' do
click_on('Edit')
- expect(page).to have_current_path(%r(one/two/three-test), ignore_query: true)
+ expect(page).to have_current_path(%r{one/two/three-test}, ignore_query: true)
expect(page).to have_content('Edit Page')
fill_in('Content', with: 'Updated Wiki Content')
@@ -149,7 +149,10 @@ RSpec.shared_examples 'User updates wiki page' do
end
end
- it_behaves_like 'edits content using the content editor', { with_expanded_references: false }
+ it_behaves_like 'edits content using the content editor', {
+ with_expanded_references: false,
+ with_quick_actions: false
+ }
it_behaves_like 'inserts diagrams.net diagram using the content editor'
it_behaves_like 'autocompletes items'
end
diff --git a/spec/support/shared_examples/features/wiki/user_views_wiki_page_shared_examples.rb b/spec/support/shared_examples/features/wiki/user_views_wiki_page_shared_examples.rb
index 254682e1a3a..3fac7e7093c 100644
--- a/spec/support/shared_examples/features/wiki/user_views_wiki_page_shared_examples.rb
+++ b/spec/support/shared_examples/features/wiki/user_views_wiki_page_shared_examples.rb
@@ -39,12 +39,12 @@ RSpec.shared_examples 'User views a wiki page' do
end
it 'shows the history of a page that has a path' do
- expect(page).to have_current_path(%r(one/two/three-test))
+ expect(page).to have_current_path(%r{one/two/three-test})
first(:link, text: 'three').click
click_on('Page history')
- expect(page).to have_current_path(%r(one/two/three-test))
+ expect(page).to have_current_path(%r{one/two/three-test})
page.within(:css, '.wiki-page-header') do
expect(page).to have_content('History')
@@ -52,7 +52,7 @@ RSpec.shared_examples 'User views a wiki page' do
end
it 'shows an old version of a page', :js do
- expect(page).to have_current_path(%r(one/two/three-test))
+ expect(page).to have_current_path(%r{one/two/three-test})
expect(find('.wiki-pages')).to have_content('three')
first(:link, text: 'three').click
@@ -61,7 +61,7 @@ RSpec.shared_examples 'User views a wiki page' do
click_on('Edit')
- expect(page).to have_current_path(%r(one/two/three-test))
+ expect(page).to have_current_path(%r{one/two/three-test})
expect(page).to have_content('Edit Page')
fill_in('Content', with: 'Updated Wiki Content')
@@ -100,7 +100,7 @@ RSpec.shared_examples 'User views a wiki page' do
click_on('image')
- expect(page).to have_current_path(%r(wikis/#{path}))
+ expect(page).to have_current_path(%r{wikis/#{path}})
end
end
@@ -109,7 +109,7 @@ RSpec.shared_examples 'User views a wiki page' do
click_on('image')
- expect(page).to have_current_path(%r(wikis/#{path}))
+ expect(page).to have_current_path(%r{wikis/#{path}})
expect(page).to have_content('Create New Page')
end
end
@@ -264,7 +264,10 @@ RSpec.shared_examples 'User views a wiki page' do
it 'opens a default wiki page', :js do
visit wiki.container.web_url
- find('.shortcuts-wiki').click
+ within_testid('super-sidebar') do
+ click_button 'Plan'
+ click_link 'Wiki'
+ end
wait_for_svg_to_be_loaded
diff --git a/spec/support/shared_examples/features/work_items_shared_examples.rb b/spec/support/shared_examples/features/work_items_shared_examples.rb
index ff79f180c64..30605c81312 100644
--- a/spec/support/shared_examples/features/work_items_shared_examples.rb
+++ b/spec/support/shared_examples/features/work_items_shared_examples.rb
@@ -8,7 +8,6 @@ RSpec.shared_examples 'work items title' do
find(title_selector).set("Work item title")
find(title_selector).native.send_keys(:return)
-
wait_for_requests
expect(work_item.reload.title).to eq 'Work item title'
@@ -16,54 +15,37 @@ RSpec.shared_examples 'work items title' do
end
RSpec.shared_examples 'work items toggle status button' do
- let(:state_button) { '[data-testid="work-item-state-toggle"]' }
-
it 'successfully shows and changes the status of the work item' do
- expect(find(state_button, match: :first)).to have_content 'Close'
-
- find(state_button, match: :first).click
-
- wait_for_requests
+ click_button 'Close', match: :first
- expect(find(state_button, match: :first)).to have_content 'Reopen'
+ expect(page).to have_button 'Reopen'
expect(work_item.reload.state).to eq('closed')
end
end
RSpec.shared_examples 'work items comments' do |type|
- let(:form_selector) { '[data-testid="work-item-add-comment"]' }
- let(:edit_button) { '[data-testid="edit-work-item-note"]' }
- let(:textarea_selector) { '[data-testid="work-item-add-comment"] #work-item-add-or-edit-comment' }
let(:is_mac) { page.evaluate_script('navigator.platform').include?('Mac') }
let(:modifier_key) { is_mac ? :command : :control }
- let(:comment) { 'Test comment' }
def set_comment
- find(form_selector).fill_in(with: comment)
+ fill_in _('Add a reply'), with: 'Test comment'
end
it 'successfully creates and shows comments' do
set_comment
-
click_button "Comment"
- wait_for_requests
-
page.within(".main-notes-list") do
- expect(page).to have_content comment
+ expect(page).to have_text 'Test comment'
end
end
it 'successfully updates existing comments' do
set_comment
click_button "Comment"
- wait_for_all_requests
-
- find(edit_button).click
+ click_button _('Edit comment')
send_keys(" updated")
- click_button "Save comment"
-
- wait_for_all_requests
+ click_button _('Save comment')
page.within(".main-notes-list") do
expect(page).to have_content "Test comment updated"
@@ -79,39 +61,31 @@ RSpec.shared_examples 'work items comments' do |type|
it 'shows work item note actions' do
set_comment
-
send_keys([modifier_key, :enter])
- wait_for_requests
page.within(".main-notes-list") do
- expect(page).to have_content comment
+ expect(page).to have_text 'Test comment'
end
page.within('.timeline-entry.note.note-wrapper.note-comment:last-child') do
- expect(page).to have_selector('[data-testid="work-item-note-actions"]')
+ click_button _('More actions')
- find('[data-testid="work-item-note-actions"]').click
-
- expect(page).to have_selector('[data-testid="copy-link-action"]')
- expect(page).to have_selector('[data-testid="assign-note-action"]')
- expect(page).to have_selector('[data-testid="delete-note-action"]')
- expect(page).to have_selector('[data-testid="edit-work-item-note"]')
+ expect(page).to have_button _('Copy link')
+ expect(page).to have_button _('Assign to commenting user')
+ expect(page).to have_button _('Delete comment')
+ expect(page).to have_button _('Edit comment')
end
end
end
it 'successfully posts comments using shortcut and checks if textarea is blank when reinitiated' do
set_comment
-
send_keys([modifier_key, :enter])
- wait_for_requests
-
page.within(".main-notes-list") do
- expect(page).to have_content comment
+ expect(page).to have_content 'Test comment'
end
-
- expect(find(textarea_selector)).to have_content ""
+ expect(page).to have_field _('Add a reply'), with: ''
end
context 'when using quick actions' do
@@ -158,9 +132,7 @@ RSpec.shared_examples 'work items comments' do |type|
end
def click_reply_and_enter_slash
- find(form_selector).fill_in(with: "/")
-
- wait_for_all_requests
+ fill_in _('Add a reply'), with: '/'
end
end
end
@@ -171,7 +143,6 @@ RSpec.shared_examples 'work items assignees' do
# The button is only when the mouse is over the input
find('[data-testid="work-item-assignees-input"]').fill_in(with: user.username)
wait_for_requests
-
# submit and simulate blur to save
send_keys(:enter)
find("body").click
@@ -182,21 +153,19 @@ RSpec.shared_examples 'work items assignees' do
it 'successfully assigns the current user by clicking `Assign myself` button' do
find('[data-testid="work-item-assignees-input"]').hover
- find('[data-testid="assign-self"]').click
- wait_for_requests
+ click_button _('Assign yourself')
expect(work_item.reload.assignees).to include(user)
end
it 'successfully removes all users on clear all button click' do
find('[data-testid="work-item-assignees-input"]').hover
- find('[data-testid="assign-self"]').click
- wait_for_requests
+ click_button _('Assign yourself')
expect(work_item.reload.assignees).to include(user)
find('[data-testid="work-item-assignees-input"]').click
- find('[data-testid="clear-all-button"]').click
+ click_button 'Clear all'
find("body").click
wait_for_requests
@@ -205,13 +174,12 @@ RSpec.shared_examples 'work items assignees' do
it 'successfully removes user on clicking badge cross button' do
find('[data-testid="work-item-assignees-input"]').hover
- find('[data-testid="assign-self"]').click
- wait_for_requests
+ click_button _('Assign yourself')
expect(work_item.reload.assignees).to include(user)
within('[data-testid="work-item-assignees-input"]') do
- find('[data-testid="close-icon"]').click
+ click_button 'Close'
end
find("body").click
wait_for_requests
@@ -228,11 +196,9 @@ RSpec.shared_examples 'work items assignees' do
end
find('[data-testid="work-item-assignees-input"]').hover
- find('[data-testid="assign-self"]').click
- wait_for_requests
+ click_button _('Assign yourself')
expect(work_item.reload.assignees).to include(user)
-
using_session :other_session do
expect(work_item.reload.assignees).to include(user)
end
@@ -246,7 +212,6 @@ RSpec.shared_examples 'work items labels' do
it 'successfully assigns a label' do
find(labels_input_selector).fill_in(with: label.title)
wait_for_requests
-
# submit and simulate blur to save
send_keys(:enter)
find(label_title_selector).click
@@ -276,7 +241,6 @@ RSpec.shared_examples 'work items labels' do
it 'removes all labels on clear all button click' do
find(labels_input_selector).fill_in(with: label.title)
wait_for_requests
-
send_keys(:enter)
find(label_title_selector).click
wait_for_requests
@@ -285,9 +249,8 @@ RSpec.shared_examples 'work items labels' do
within(labels_input_selector) do
find('input').click
- find('[data-testid="clear-all-button"]').click
+ click_button 'Clear all'
end
-
find(label_title_selector).click
wait_for_requests
@@ -297,7 +260,6 @@ RSpec.shared_examples 'work items labels' do
it 'removes label on clicking badge cross button' do
find(labels_input_selector).fill_in(with: label.title)
wait_for_requests
-
send_keys(:enter)
find(label_title_selector).click
wait_for_requests
@@ -305,9 +267,8 @@ RSpec.shared_examples 'work items labels' do
expect(page).to have_text(label.title)
within(labels_input_selector) do
- find('[data-testid="close-icon"]').click
+ click_button 'Remove label'
end
-
find(label_title_selector).click
wait_for_requests
@@ -324,7 +285,6 @@ RSpec.shared_examples 'work items labels' do
find(labels_input_selector).fill_in(with: label.title)
wait_for_requests
-
send_keys(:enter)
find(label_title_selector).click
wait_for_requests
@@ -341,10 +301,7 @@ end
RSpec.shared_examples 'work items description' do
it 'shows GFM autocomplete', :aggregate_failures do
click_button "Edit description"
-
- find('[aria-label="Description"]').send_keys("@#{user.username}")
-
- wait_for_requests
+ fill_in _('Description'), with: "@#{user.username}"
page.within('.atwho-container') do
expect(page).to have_text(user.name)
@@ -353,10 +310,7 @@ RSpec.shared_examples 'work items description' do
it 'autocompletes available quick actions', :aggregate_failures do
click_button "Edit description"
-
- find('[aria-label="Description"]').send_keys("/")
-
- wait_for_requests
+ fill_in _('Description'), with: '/'
page.within('#at-view-commands') do
expect(page).to have_text("title")
@@ -378,8 +332,6 @@ RSpec.shared_examples 'work items description' do
it 'shows conflict message when description changes', :aggregate_failures do
click_button "Edit description"
- wait_for_requests
-
::WorkItems::UpdateService.new(
container: work_item.project,
current_user: other_user,
@@ -388,11 +340,11 @@ RSpec.shared_examples 'work items description' do
wait_for_requests
- find('[aria-label="Description"]').send_keys("oh yeah!")
+ fill_in _('Description'), with: 'oh yeah!'
- expect(page.find('[data-testid="work-item-description-conflicts"]')).to have_text(expected_warning)
+ expect(page).to have_text(expected_warning)
- click_button "Save and overwrite"
+ click_button s_('WorkItem|Save and overwrite')
expect(page.find('[data-testid="work-item-description"]')).to have_text("oh yeah!")
end
@@ -410,32 +362,23 @@ RSpec.shared_examples 'work items invite members' do
click_button('Invite members')
page.within invite_modal_selector do
- expect(page).to have_content("You're inviting members to the #{work_item.project.name} project")
+ expect(page).to have_text("You're inviting members to the #{work_item.project.name} project")
end
end
end
RSpec.shared_examples 'work items milestone' do
- def set_milestone(milestone_dropdown, milestone_text)
- milestone_dropdown.click
-
- find('[data-testid="work-item-milestone-dropdown"] .gl-form-input', visible: true).send_keys "\"#{milestone_text}\""
- wait_for_requests
-
- click_button(milestone_text)
- wait_for_requests
- end
-
- let(:milestone_dropdown_selector) { '[data-testid="work-item-milestone-dropdown"]' }
-
it 'searches and sets or removes milestone for the work item' do
- set_milestone(find(milestone_dropdown_selector), milestone.title)
+ click_button s_('WorkItem|Add to milestone')
+ send_keys "\"#{milestone.title}\""
+ select_listbox_item(milestone.title, exact_text: true)
- expect(page.find(milestone_dropdown_selector)).to have_text(milestone.title)
+ expect(page).to have_button(milestone.title)
- set_milestone(find(milestone_dropdown_selector), 'No milestone')
+ click_button milestone.title
+ select_listbox_item(s_('WorkItem|No milestone'), exact_text: true)
- expect(page.find(milestone_dropdown_selector)).to have_text('Add to milestone')
+ expect(page).to have_button(s_('WorkItem|Add to milestone'))
end
end
@@ -443,70 +386,52 @@ RSpec.shared_examples 'work items comment actions for guest users' do
context 'for guest user' do
it 'hides other actions other than copy link' do
page.within(".main-notes-list") do
- expect(page).to have_selector('[data-testid="work-item-note-actions"]')
+ click_button _('More actions'), match: :first
- find('[data-testid="work-item-note-actions"]', match: :first).click
- expect(page).to have_selector('[data-testid="copy-link-action"]')
- expect(page).not_to have_selector('[data-testid="assign-note-action"]')
+ expect(page).to have_button _('Copy link')
+ expect(page).not_to have_button _('Assign to commenting user')
end
end
end
end
RSpec.shared_examples 'work items notifications' do
- let(:actions_dropdown_selector) { '[data-testid="work-item-actions-dropdown"]' }
- let(:notifications_toggle_selector) { '[data-testid="notifications-toggle-action"] button[role="switch"]' }
-
it 'displays toast when notification is toggled' do
- find(actions_dropdown_selector).click
+ click_button _('More actions'), match: :first
- page.within('[data-testid="notifications-toggle-form"]') do
- expect(page).not_to have_css(".is-checked")
+ within_testid('notifications-toggle-form') do
+ expect(page).not_to have_button(class: 'gl-toggle is-checked')
- find(notifications_toggle_selector).click
- wait_for_requests
+ click_button(class: 'gl-toggle')
- expect(page).to have_css(".is-checked")
+ expect(page).to have_button(class: 'gl-toggle is-checked')
end
- page.within('.gl-toast') do
- expect(find('.toast-body')).to have_content(_('Notifications turned on.'))
- end
+ expect(page).to have_css('.gl-toast', text: _('Notifications turned on.'))
end
end
RSpec.shared_examples 'work items todos' do
- let(:todos_action_selector) { '[data-testid="work-item-todos-action"]' }
- let(:todos_icon_selector) { '[data-testid="work-item-todos-icon"]' }
- let(:header_section_selector) { '[data-testid="work-item-body"]' }
-
- def toggle_todo_action
- find(todos_action_selector).click
- wait_for_requests
- end
-
it 'adds item to the list' do
- page.within(header_section_selector) do
- expect(find(todos_action_selector)['aria-label']).to eq('Add a to do')
+ expect(page).to have_button s_('WorkItem|Add a to do')
- toggle_todo_action
+ click_button s_('WorkItem|Add a to do')
- expect(find(todos_action_selector)['aria-label']).to eq('Mark as done')
- end
+ expect(page).to have_button s_('WorkItem|Mark as done')
- page.within ".header-content span[aria-label='#{_('Todos count')}']" do
+ within_testid('todos-shortcut-button') do
expect(page).to have_content '1'
end
end
it 'marks a todo as done' do
- page.within(header_section_selector) do
- toggle_todo_action
- toggle_todo_action
- end
+ click_button s_('WorkItem|Add a to do')
+ click_button s_('WorkItem|Mark as done')
- expect(find(todos_action_selector)['aria-label']).to eq('Add a to do')
- expect(page).to have_selector(".header-content span[aria-label='#{_('Todos count')}']", visible: :hidden)
+ expect(page).to have_button s_('WorkItem|Add a to do')
+ within_testid('todos-shortcut-button') do
+ expect(page).to have_content("")
+ end
end
end
@@ -514,8 +439,7 @@ RSpec.shared_examples 'work items award emoji' do
let(:award_section_selector) { '.awards' }
let(:award_button_selector) { '[data-testid="award-button"]' }
let(:selected_award_button_selector) { '[data-testid="award-button"].selected' }
- let(:emoji_picker_button_selector) { '[data-testid="emoji-picker"]' }
- let(:basketball_emoji_selector) { 'gl-emoji[data-name="basketball"]' }
+ let(:grinning_emoji_selector) { 'gl-emoji[data-name="grinning"]' }
let(:tooltip_selector) { '.gl-tooltip' }
def select_emoji
@@ -560,10 +484,41 @@ RSpec.shared_examples 'work items award emoji' do
it 'add custom award to the work item for current user' do
within(award_section_selector) do
- find(emoji_picker_button_selector).click
- find(basketball_emoji_selector).click
+ click_button _('Add reaction')
+ find(grinning_emoji_selector).click
+
+ expect(page).to have_selector(grinning_emoji_selector)
+ end
+ end
+end
+
+RSpec.shared_examples 'work items parent' do |type|
+ let(:work_item_parent) { create(:work_item, type, project: project) }
+
+ def set_parent(parent_dropdown, parent_text)
+ parent_dropdown.click
+
+ find('[data-testid="listbox-search-input"] .gl-listbox-search-input',
+ visible: true).send_keys "\"#{parent_text}\""
+ wait_for_requests
+
+ find('.gl-new-dropdown-item').click
+ wait_for_requests
+ end
+
+ let(:parent_dropdown_selector) { 'work-item-parent-listbox' }
+
+ it 'searches and sets or removes parent for the work item' do
+ within_testid('work-item-parent-form') do
+ set_parent(find_by_testid(parent_dropdown_selector), work_item_parent.title)
+
+ expect(find_by_testid(parent_dropdown_selector)).to have_text(work_item_parent.title)
+
+ find_by_testid(parent_dropdown_selector).click
+
+ click_button('Unassign')
- expect(page).to have_selector(basketball_emoji_selector)
+ expect(find_by_testid(parent_dropdown_selector)).to have_text('None')
end
end
end
diff --git a/spec/support/shared_examples/finders/issues_finder_shared_examples.rb b/spec/support/shared_examples/finders/issues_finder_shared_examples.rb
index ed8feebf1f6..043d6db66d3 100644
--- a/spec/support/shared_examples/finders/issues_finder_shared_examples.rb
+++ b/spec/support/shared_examples/finders/issues_finder_shared_examples.rb
@@ -956,7 +956,7 @@ RSpec.shared_examples 'issues or work items finder' do |factory, execute_context
end
context 'multiple params' do
- let(:params) { { issue_types: %w(issue incident) } }
+ let(:params) { { issue_types: %w[issue incident] } }
it 'returns all items' do
expect(items).to contain_exactly(incident_item, item1, item2, item3, item4, item5)
diff --git a/spec/support/shared_examples/graphql/notes_quick_actions_for_work_items_shared_examples.rb b/spec/support/shared_examples/graphql/notes_quick_actions_for_work_items_shared_examples.rb
index 0577ac329e6..9af9aaef483 100644
--- a/spec/support/shared_examples/graphql/notes_quick_actions_for_work_items_shared_examples.rb
+++ b/spec/support/shared_examples/graphql/notes_quick_actions_for_work_items_shared_examples.rb
@@ -70,7 +70,7 @@ RSpec.shared_examples 'work item supports labels widget updates via quick action
let(:add_label_ids) { [] }
let(:remove_label_ids) { [] }
- before_all do
+ before do
noteable.update!(labels: [existing_label])
end
diff --git a/spec/support/shared_examples/graphql/resolvers/data_transfer_resolver_shared_examples.rb b/spec/support/shared_examples/graphql/resolvers/data_transfer_resolver_shared_examples.rb
index 8551bd052ce..c50e0434eb1 100644
--- a/spec/support/shared_examples/graphql/resolvers/data_transfer_resolver_shared_examples.rb
+++ b/spec/support/shared_examples/graphql/resolvers/data_transfer_resolver_shared_examples.rb
@@ -1,16 +1,6 @@
# frozen_string_literal: true
RSpec.shared_examples 'Data transfer resolver' do
- it 'returns mock data' do |_query_object|
- mocked_data = ['mocked_data']
-
- allow_next_instance_of(DataTransfer::MockedTransferFinder) do |instance|
- allow(instance).to receive(:execute).and_return(mocked_data)
- end
-
- expect(resolve_egress[:egress_nodes]).to eq(mocked_data)
- end
-
context 'when data_transfer_monitoring is disabled' do
before do
stub_feature_flags(data_transfer_monitoring: false)
diff --git a/spec/support/shared_examples/graphql/resolvers/packages_resolvers_shared_examples.rb b/spec/support/shared_examples/graphql/resolvers/packages_resolvers_shared_examples.rb
index aadc061f175..98eadc507d7 100644
--- a/spec/support/shared_examples/graphql/resolvers/packages_resolvers_shared_examples.rb
+++ b/spec/support/shared_examples/graphql/resolvers/packages_resolvers_shared_examples.rb
@@ -52,6 +52,18 @@ RSpec.shared_examples 'group and projects packages resolver' do
it { is_expected.to eq([conan_package]) }
end
+ context 'filter by package_version' do
+ let(:args) { { package_version: '1.0.0', sort: 'CREATED_DESC' } }
+
+ it { is_expected.to eq([conan_package]) }
+
+ it 'includes_versionless has no effect' do
+ args[:include_versionless] = true
+
+ is_expected.to eq([conan_package])
+ end
+ end
+
context 'filter by status' do
let(:args) { { status: 'error', sort: 'CREATED_DESC' } }
diff --git a/spec/support/shared_examples/graphql/resolvers/users/pages_visits_resolvers_shared_examples.rb b/spec/support/shared_examples/graphql/resolvers/users/pages_visits_resolvers_shared_examples.rb
new file mode 100644
index 00000000000..0dca28a4e74
--- /dev/null
+++ b/spec/support/shared_examples/graphql/resolvers/users/pages_visits_resolvers_shared_examples.rb
@@ -0,0 +1,39 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'namespace visits resolver' do
+ include GraphqlHelpers
+
+ describe '#resolve' do
+ context 'when user is not logged in' do
+ let_it_be(:current_user) { nil }
+
+ it 'returns nil' do
+ expect(resolve_items).to eq(nil)
+ end
+ end
+
+ context 'when user is logged in' do
+ let_it_be(:current_user) { create(:user) }
+
+ context 'when the frecent_namespaces_suggestions feature flag is disabled' do
+ before do
+ stub_feature_flags(frecent_namespaces_suggestions: false)
+ end
+
+ it 'raises a "Resource not available" exception' do
+ expect(resolve_items).to be_a(::Gitlab::Graphql::Errors::ResourceNotAvailable)
+ end
+ end
+
+ it 'returns frecent groups' do
+ expect(resolve_items).to be_an_instance_of(Array)
+ end
+ end
+ end
+
+ private
+
+ def resolve_items
+ sync(resolve(described_class, ctx: { current_user: current_user }))
+ end
+end
diff --git a/spec/support/shared_examples/graphql/types/merge_request_interactions_type_shared_examples.rb b/spec/support/shared_examples/graphql/types/merge_request_interactions_type_shared_examples.rb
index c32e758d921..7a3b3d6924c 100644
--- a/spec/support/shared_examples/graphql/types/merge_request_interactions_type_shared_examples.rb
+++ b/spec/support/shared_examples/graphql/types/merge_request_interactions_type_shared_examples.rb
@@ -28,6 +28,7 @@ RSpec.shared_examples "a user type with merge request interaction type" do
authoredMergeRequests
assignedMergeRequests
reviewRequestedMergeRequests
+ organizations
groupMemberships
groupCount
projectMemberships
@@ -50,6 +51,7 @@ RSpec.shared_examples "a user type with merge request interaction type" do
organization
jobTitle
createdAt
+ lastActivityOn
pronouns
ide
]
diff --git a/spec/support/shared_examples/lib/banzai/filters/sanitization_filter_shared_examples.rb b/spec/support/shared_examples/lib/banzai/filters/sanitization_filter_shared_examples.rb
index e6433f963f4..e335255e426 100644
--- a/spec/support/shared_examples/lib/banzai/filters/sanitization_filter_shared_examples.rb
+++ b/spec/support/shared_examples/lib/banzai/filters/sanitization_filter_shared_examples.rb
@@ -2,14 +2,14 @@
RSpec.shared_examples 'default allowlist' do
it 'sanitizes tags that are not allowed' do
- act = %q{<textarea>no inputs</textarea> and <blink>no blinks</blink>}
+ act = %q(<textarea>no inputs</textarea> and <blink>no blinks</blink>)
exp = 'no inputs and no blinks'
expect(filter(act).to_html).to eq exp
end
it 'sanitizes tag attributes' do
- act = %q{<a href="http://example.com/bar.html" onclick="bar">Text</a>}
- exp = %q{<a href="http://example.com/bar.html">Text</a>}
+ act = %q(<a href="http://example.com/bar.html" onclick="bar">Text</a>)
+ exp = %q(<a href="http://example.com/bar.html">Text</a>)
expect(filter(act).to_html).to eq exp
end
@@ -31,13 +31,13 @@ RSpec.shared_examples 'default allowlist' do
end
it 'sanitizes `class` attribute on any element' do
- act = %q{<strong class="foo">Strong</strong>}
- expect(filter(act).to_html).to eq %q{<strong>Strong</strong>}
+ act = %q(<strong class="foo">Strong</strong>)
+ expect(filter(act).to_html).to eq %q(<strong>Strong</strong>)
end
it 'sanitizes `id` attribute on any element' do
- act = %q{<em id="foo">Emphasis</em>}
- expect(filter(act).to_html).to eq %q{<em>Emphasis</em>}
+ act = %q(<em id="foo">Emphasis</em>)
+ expect(filter(act).to_html).to eq %q(<em>Emphasis</em>)
end
end
@@ -150,8 +150,8 @@ end
RSpec.shared_examples 'sanitize link' do
it 'removes `rel` attribute from `a` elements' do
- act = %q{<a href="#" rel="nofollow">Link</a>}
- exp = %q{<a href="#">Link</a>}
+ act = %q(<a href="#" rel="nofollow">Link</a>)
+ exp = %q(<a href="#">Link</a>)
expect(filter(act).to_html).to eq exp
end
@@ -167,14 +167,14 @@ RSpec.shared_examples 'sanitize link' do
end
it 'allows non-standard anchor schemes' do
- exp = %q{<a href="irc://irc.freenode.net/git">IRC</a>}
+ exp = %q(<a href="irc://irc.freenode.net/git">IRC</a>)
act = filter(exp)
expect(act.to_html).to eq exp
end
it 'allows relative links' do
- exp = %q{<a href="foo/bar.md">foo/bar.md</a>}
+ exp = %q(<a href="foo/bar.md">foo/bar.md</a>)
act = filter(exp)
expect(act.to_html).to eq exp
diff --git a/spec/support/shared_examples/lib/gitlab/import/advance_stage_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/import/advance_stage_shared_examples.rb
index effa6a6f6f0..c172e73ce9e 100644
--- a/spec/support/shared_examples/lib/gitlab/import/advance_stage_shared_examples.rb
+++ b/spec/support/shared_examples/lib/gitlab/import/advance_stage_shared_examples.rb
@@ -4,7 +4,7 @@ RSpec.shared_examples Gitlab::Import::AdvanceStage do |factory:|
let_it_be(:project) { create(:project) }
let_it_be_with_reload(:import_state) { create(factory, :started, project: project, jid: '123') }
let(:worker) { described_class.new }
- let(:next_stage) { :finish }
+ let(:next_stage) { 'finish' }
describe '#perform', :clean_gitlab_redis_shared_state do
context 'when the project no longer exists' do
@@ -60,7 +60,7 @@ RSpec.shared_examples Gitlab::Import::AdvanceStage do |factory:|
end
it 'schedules the next stage' do
- next_worker = described_class::STAGES[next_stage]
+ next_worker = described_class::STAGES[next_stage.to_sym]
expect_next_found_instance_of(import_state.class) do |state|
expect(state).to receive(:refresh_jid_expiration)
@@ -72,7 +72,7 @@ RSpec.shared_examples Gitlab::Import::AdvanceStage do |factory:|
end
it 'raises KeyError when the stage name is invalid' do
- expect { worker.perform(project.id, { '123' => 2 }, :kittens) }
+ expect { worker.perform(project.id, { '123' => 2 }, 'kittens') }
.to raise_error(KeyError)
end
end
@@ -106,7 +106,7 @@ RSpec.shared_examples Gitlab::Import::AdvanceStage do |factory:|
it 'advances to next stage' do
freeze_time do
- next_worker = described_class::STAGES[next_stage]
+ next_worker = described_class::STAGES[next_stage.to_sym]
expect(next_worker).to receive(:perform_async).with(project.id)
@@ -122,7 +122,7 @@ RSpec.shared_examples Gitlab::Import::AdvanceStage do |factory:|
it 'logs error and fails import' do
freeze_time do
- next_worker = described_class::STAGES[next_stage]
+ next_worker = described_class::STAGES[next_stage.to_sym]
expect(next_worker).not_to receive(:perform_async).with(project.id)
expect_next_instance_of(described_class) do |klass|
diff --git a/spec/support/shared_examples/lib/gitlab/middleware/multipart_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/middleware/multipart_shared_examples.rb
index 16b048ae325..c17d022293d 100644
--- a/spec/support/shared_examples/lib/gitlab/middleware/multipart_shared_examples.rb
+++ b/spec/support/shared_examples/lib/gitlab/middleware/multipart_shared_examples.rb
@@ -8,7 +8,7 @@ RSpec.shared_examples 'handling all upload parameters conditions' do
let(:params) { upload_parameters_for(filepath: uploaded_filepath, key: 'file', mode: mode, filename: filename, remote_id: remote_id) }
it 'builds an UploadedFile' do
- expect_uploaded_files(filepath: uploaded_filepath, original_filename: filename, remote_id: remote_id, size: uploaded_file.size, params_path: %w(file))
+ expect_uploaded_files(filepath: uploaded_filepath, original_filename: filename, remote_id: remote_id, size: uploaded_file.size, params_path: %w[file])
subject
end
@@ -27,8 +27,8 @@ RSpec.shared_examples 'handling all upload parameters conditions' do
it 'builds UploadedFiles' do
expect_uploaded_files(
[
- { filepath: uploaded_filepath, original_filename: filename, remote_id: remote_id, size: uploaded_file.size, params_path: %w(file1) },
- { filepath: uploaded_filepath2, original_filename: filename2, remote_id: remote_id2, size: uploaded_file2.size, params_path: %w(file2) }
+ { filepath: uploaded_filepath, original_filename: filename, remote_id: remote_id, size: uploaded_file.size, params_path: %w[file1] },
+ { filepath: uploaded_filepath2, original_filename: filename2, remote_id: remote_id2, size: uploaded_file2.size, params_path: %w[file2] }
]
)
@@ -43,7 +43,7 @@ RSpec.shared_examples 'handling all upload parameters conditions' do
let(:params) { { 'user' => { 'avatar' => upload_parameters_for(filepath: uploaded_filepath, mode: mode, filename: filename, remote_id: remote_id) } } }
it 'builds an UploadedFile' do
- expect_uploaded_files(filepath: uploaded_filepath, original_filename: filename, remote_id: remote_id, size: uploaded_file.size, params_path: %w(user avatar))
+ expect_uploaded_files(filepath: uploaded_filepath, original_filename: filename, remote_id: remote_id, size: uploaded_file.size, params_path: %w[user avatar])
subject
end
@@ -65,8 +65,8 @@ RSpec.shared_examples 'handling all upload parameters conditions' do
it 'builds UploadedFiles' do
expect_uploaded_files(
[
- { filepath: uploaded_filepath, original_filename: filename, remote_id: remote_id, size: uploaded_file.size, params_path: %w(user avatar) },
- { filepath: uploaded_filepath2, original_filename: filename2, remote_id: remote_id2, size: uploaded_file2.size, params_path: %w(user screenshot) }
+ { filepath: uploaded_filepath, original_filename: filename, remote_id: remote_id, size: uploaded_file.size, params_path: %w[user avatar] },
+ { filepath: uploaded_filepath2, original_filename: filename2, remote_id: remote_id2, size: uploaded_file2.size, params_path: %w[user screenshot] }
]
)
@@ -81,7 +81,7 @@ RSpec.shared_examples 'handling all upload parameters conditions' do
let(:params) { { 'user' => { 'avatar' => { 'bananas' => upload_parameters_for(filepath: uploaded_filepath, mode: mode, filename: filename, remote_id: remote_id) } } } }
it 'builds an UploadedFile' do
- expect_uploaded_files(filepath: uploaded_file, original_filename: filename, remote_id: remote_id, size: uploaded_file.size, params_path: %w(user avatar bananas))
+ expect_uploaded_files(filepath: uploaded_file, original_filename: filename, remote_id: remote_id, size: uploaded_file.size, params_path: %w[user avatar bananas])
subject
end
@@ -107,8 +107,8 @@ RSpec.shared_examples 'handling all upload parameters conditions' do
it 'builds UploadedFiles' do
expect_uploaded_files(
[
- { filepath: uploaded_file, original_filename: filename, remote_id: remote_id, size: uploaded_file.size, params_path: %w(user avatar bananas) },
- { filepath: uploaded_file2, original_filename: filename2, remote_id: remote_id2, size: uploaded_file2.size, params_path: %w(user friend ananas) }
+ { filepath: uploaded_file, original_filename: filename, remote_id: remote_id, size: uploaded_file.size, params_path: %w[user avatar bananas] },
+ { filepath: uploaded_file2, original_filename: filename2, remote_id: remote_id2, size: uploaded_file2.size, params_path: %w[user friend ananas] }
]
)
@@ -141,9 +141,9 @@ RSpec.shared_examples 'handling all upload parameters conditions' do
it 'builds UploadedFiles' do
expect_uploaded_files(
[
- { filepath: uploaded_filepath, original_filename: filename, remote_id: remote_id, size: uploaded_file.size, params_path: %w(file) },
- { filepath: uploaded_filepath2, original_filename: filename2, remote_id: remote_id2, size: uploaded_file2.size, params_path: %w(user avatar) },
- { filepath: uploaded_filepath3, original_filename: filename3, remote_id: remote_id3, size: uploaded_file3.size, params_path: %w(user friend avatar) }
+ { filepath: uploaded_filepath, original_filename: filename, remote_id: remote_id, size: uploaded_file.size, params_path: %w[file] },
+ { filepath: uploaded_filepath2, original_filename: filename2, remote_id: remote_id2, size: uploaded_file2.size, params_path: %w[user avatar] },
+ { filepath: uploaded_filepath3, original_filename: filename3, remote_id: remote_id3, size: uploaded_file3.size, params_path: %w[user friend avatar] }
]
)
diff --git a/spec/support/shared_examples/lib/sbom/package_url_shared_examples.rb b/spec/support/shared_examples/lib/sbom/package_url_shared_examples.rb
new file mode 100644
index 00000000000..84faba20d31
--- /dev/null
+++ b/spec/support/shared_examples/lib/sbom/package_url_shared_examples.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'purl_types enum' do
+ let(:purl_types) do
+ {
+ composer: 1,
+ conan: 2,
+ gem: 3,
+ golang: 4,
+ maven: 5,
+ npm: 6,
+ nuget: 7,
+ pypi: 8,
+ apk: 9,
+ rpm: 10,
+ deb: 11,
+ 'cbl-mariner': 12,
+ wolfi: 13
+ }
+ end
+
+ it { is_expected.to define_enum_for(:purl_type).with_values(purl_types) }
+end
diff --git a/spec/support/shared_examples/lib/wikis_api_examples.rb b/spec/support/shared_examples/lib/wikis_api_examples.rb
index c57ac328a60..162a2b8ea49 100644
--- a/spec/support/shared_examples/lib/wikis_api_examples.rb
+++ b/spec/support/shared_examples/lib/wikis_api_examples.rb
@@ -53,7 +53,7 @@ RSpec.shared_examples_for 'wikis API returns wiki page' do
specify do
expect(response).to have_gitlab_http_status(:ok)
- expect(json_response.size).to eq(5)
+ expect(json_response.size).to eq(6)
expect(json_response.keys).to match_array(expected_keys_with_content)
expect(json_response['content']).to eq(expected_content)
expect(json_response['slug']).to eq(page.slug)
@@ -118,7 +118,7 @@ RSpec.shared_examples_for 'wikis API creates wiki page' do
post(api(url, user), params: payload)
expect(response).to have_gitlab_http_status(:created)
- expect(json_response.size).to eq(5)
+ expect(json_response.size).to eq(6)
expect(json_response.keys).to match_array(expected_keys_with_content)
expect(json_response['content']).to eq(payload[:content])
expect(json_response['slug']).to eq(payload[:title].tr(' ', '-'))
@@ -145,7 +145,7 @@ RSpec.shared_examples_for 'wikis API updates wiki page' do
put(api(url, user), params: payload)
expect(response).to have_gitlab_http_status(:ok)
- expect(json_response.size).to eq(5)
+ expect(json_response.size).to eq(6)
expect(json_response.keys).to match_array(expected_keys_with_content)
expect(json_response['content']).to eq(payload[:content])
expect(json_response['slug']).to eq(payload[:title].tr(' ', '-'))
diff --git a/spec/support/shared_examples/mailers/notify_shared_examples.rb b/spec/support/shared_examples/mailers/notify_shared_examples.rb
index 987060d73b9..f0d89f6ffaa 100644
--- a/spec/support/shared_examples/mailers/notify_shared_examples.rb
+++ b/spec/support/shared_examples/mailers/notify_shared_examples.rb
@@ -230,6 +230,8 @@ end
RSpec.shared_examples 'an email with a labels subscriptions link in its footer' do
it { is_expected.to have_body_text('label subscriptions') }
+
+ it { is_expected.to have_body_text(%(href="#{project_labels_url(project, subscribed: true)}")) }
end
RSpec.shared_examples 'a note email' do
diff --git a/spec/support/shared_examples/metrics/transaction_metrics_with_labels_shared_examples.rb b/spec/support/shared_examples/metrics/transaction_metrics_with_labels_shared_examples.rb
index 286c60f1f4f..3fc52cdd86e 100644
--- a/spec/support/shared_examples/metrics/transaction_metrics_with_labels_shared_examples.rb
+++ b/spec/support/shared_examples/metrics/transaction_metrics_with_labels_shared_examples.rb
@@ -95,7 +95,7 @@ RSpec.shared_examples 'transaction metrics with labels' do
expect(prometheus_metric).to receive(:increment).with(labels.merge(sane: 'yes'), 1)
transaction_obj.increment(:block_labels, 1, sane: 'yes') do
- label_keys %i(sane)
+ label_keys %i[sane]
end
end
@@ -145,7 +145,7 @@ RSpec.shared_examples 'transaction metrics with labels' do
expect(prometheus_metric).to receive(:set).with(labels.merge(sane: 'yes'), 99)
transaction_obj.set(:block_labels_set, 99, sane: 'yes') do
- label_keys %i(sane)
+ label_keys %i[sane]
end
end
@@ -195,7 +195,7 @@ RSpec.shared_examples 'transaction metrics with labels' do
expect(prometheus_metric).to receive(:observe).with(labels.merge(sane: 'yes'), 2.0)
transaction_obj.observe(:block_labels_observe, 2.0, sane: 'yes') do
- label_keys %i(sane)
+ label_keys %i[sane]
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 6e7d04d3cba..70179dd7fc7 100644
--- a/spec/support/shared_examples/models/application_setting_shared_examples.rb
+++ b/spec/support/shared_examples/models/application_setting_shared_examples.rb
@@ -246,7 +246,7 @@ RSpec.shared_examples 'application settings examples' do
context 'in FIPS mode', :fips_mode do
it 'excludes DSA from supported key types' do
- expect(setting.allowed_key_types).to contain_exactly(*Gitlab::SSHPublicKey.supported_types - %i(dsa))
+ expect(setting.allowed_key_types).to contain_exactly(*Gitlab::SSHPublicKey.supported_types - %i[dsa])
end
end
@@ -297,7 +297,7 @@ RSpec.shared_examples 'application settings examples' do
describe '#pick_repository_storage' do
before do
- allow(Gitlab.config.repositories.storages).to receive(:keys).and_return(%w(default backup))
+ allow(Gitlab.config.repositories.storages).to receive(:keys).and_return(%w[default backup])
allow(setting).to receive(:repository_storages_weighted).and_return({ 'default' => 20, 'backup' => 80 })
end
@@ -314,13 +314,13 @@ RSpec.shared_examples 'application settings examples' do
using RSpec::Parameterized::TableSyntax
where(:config_storages, :storages, :normalized) do
- %w(default backup) | { 'default' => 0, 'backup' => 100 } | { 'default' => 0.0, 'backup' => 1.0 }
- %w(default backup) | { 'default' => 100, 'backup' => 100 } | { 'default' => 0.5, 'backup' => 0.5 }
- %w(default backup) | { 'default' => 20, 'backup' => 80 } | { 'default' => 0.2, 'backup' => 0.8 }
- %w(default backup) | { 'default' => 0, 'backup' => 0 } | { 'default' => 0.0, 'backup' => 0.0 }
- %w(default) | { 'default' => 0, 'backup' => 100 } | { 'default' => 0.0 }
- %w(default) | { 'default' => 100, 'backup' => 100 } | { 'default' => 1.0 }
- %w(default) | { 'default' => 20, 'backup' => 80 } | { 'default' => 1.0 }
+ %w[default backup] | { 'default' => 0, 'backup' => 100 } | { 'default' => 0.0, 'backup' => 1.0 }
+ %w[default backup] | { 'default' => 100, 'backup' => 100 } | { 'default' => 0.5, 'backup' => 0.5 }
+ %w[default backup] | { 'default' => 20, 'backup' => 80 } | { 'default' => 0.2, 'backup' => 0.8 }
+ %w[default backup] | { 'default' => 0, 'backup' => 0 } | { 'default' => 0.0, 'backup' => 0.0 }
+ %w[default] | { 'default' => 0, 'backup' => 100 } | { 'default' => 0.0 }
+ %w[default] | { 'default' => 100, 'backup' => 100 } | { 'default' => 1.0 }
+ %w[default] | { 'default' => 20, 'backup' => 80 } | { 'default' => 1.0 }
end
with_them do
diff --git a/spec/support/shared_examples/models/concerns/can_move_repository_storage_shared_examples.rb b/spec/support/shared_examples/models/concerns/can_move_repository_storage_shared_examples.rb
index 8deeecea30d..77327e9b539 100644
--- a/spec/support/shared_examples/models/concerns/can_move_repository_storage_shared_examples.rb
+++ b/spec/support/shared_examples/models/concerns/can_move_repository_storage_shared_examples.rb
@@ -42,6 +42,12 @@ RSpec.shared_examples 'can move repository storage' do
.to change { container.repository_read_only? }
.from(true).to(false)
end
+
+ it 'raises an error when the update fails' do
+ expect(container).to receive(:update_repository_read_only_column).and_return(false)
+
+ expect { container.set_repository_writable! }.to raise_error(ActiveRecord::RecordNotSaved, /Database update failed/)
+ end
end
describe '#reference_counter' do
diff --git a/spec/support/shared_examples/models/concerns/repository_storage_movable_shared_examples.rb b/spec/support/shared_examples/models/concerns/repository_storage_movable_shared_examples.rb
index a9a13ddcd60..d8a8d1e1cea 100644
--- a/spec/support/shared_examples/models/concerns/repository_storage_movable_shared_examples.rb
+++ b/spec/support/shared_examples/models/concerns/repository_storage_movable_shared_examples.rb
@@ -5,6 +5,19 @@ RSpec.shared_examples 'handles repository moves' do
it { is_expected.to belong_to(:container) }
end
+ describe 'scopes' do
+ describe '.scheduled_or_started' do
+ subject { described_class.scheduled_or_started }
+
+ let!(:initial) { create(repository_storage_factory_key, state: 1) }
+ let!(:scheduled) { create(repository_storage_factory_key, state: 2) }
+ let!(:started) { create(repository_storage_factory_key, state: 3) }
+ let!(:finished) { create(repository_storage_factory_key, state: 4) }
+
+ it { is_expected.to contain_exactly(scheduled, started) }
+ end
+ end
+
describe 'validations' do
it { is_expected.to validate_presence_of(:container) }
it { is_expected.to validate_presence_of(:state) }
@@ -59,9 +72,9 @@ RSpec.shared_examples 'handles repository moves' do
end
context 'when in the default state' do
- subject(:storage_move) { create(repository_storage_factory_key, container: container, destination_storage_name: 'test_second_storage') }
+ let!(:storage_move) { create(repository_storage_factory_key, container: container, destination_storage_name: 'test_second_storage') }
- context 'and transits to scheduled' do
+ context 'and transitions to scheduled' do
it 'triggers the corresponding repository storage worker' do
expect(repository_storage_worker).to receive(:perform_async).with(container.id, 'test_second_storage', storage_move.id)
@@ -77,31 +90,37 @@ RSpec.shared_examples 'handles repository moves' do
it 'does not trigger the corresponding repository storage worker and adds an error' do
expect(repository_storage_worker).not_to receive(:perform_async)
+
storage_move.schedule!
+
expect(storage_move.errors[error_key]).to include('foobar')
end
it 'sets the state to failed' do
expect(storage_move).to receive(:do_fail!).and_call_original
+
storage_move.schedule!
+
expect(storage_move.state_name).to eq(:failed)
+ expect(container).not_to be_repository_read_only
end
end
end
- context 'and transits to started' do
+ context 'and transitions to started' do
it 'does not allow the transition' do
- expect { storage_move.start! }
- .to raise_error(StateMachines::InvalidTransition)
+ expect { storage_move.start! }.to raise_error(StateMachines::InvalidTransition)
end
end
end
context 'when started' do
- subject(:storage_move) { create(repository_storage_factory_key, :started, container: container, destination_storage_name: 'test_second_storage') }
+ let!(:storage_move) { create(repository_storage_factory_key, :started, container: container, destination_storage_name: 'test_second_storage') }
- context 'and transits to replicated' do
+ context 'and transitions to replicated' do
it 'marks the container as writable' do
+ container.set_repository_read_only!
+
storage_move.finish_replication!
expect(container).not_to be_repository_read_only
@@ -109,12 +128,29 @@ RSpec.shared_examples 'handles repository moves' do
it 'updates the updated_at column of the container', :aggregate_failures do
expect { storage_move.finish_replication! }.to change { container.updated_at }
+
expect(storage_move.container.updated_at).to be >= storage_move.updated_at
end
end
- context 'and transits to failed' do
+ context 'and transitions to failed' do
it 'marks the container as writable' do
+ container.set_repository_read_only!
+
+ storage_move.do_fail!
+
+ expect(container).not_to be_repository_read_only
+ end
+ end
+ end
+
+ context 'when replicated' do
+ let!(:storage_move) { create(repository_storage_factory_key, :replicated, container: container, destination_storage_name: 'test_second_storage') }
+
+ context 'and transitions to cleanup_failed' do
+ it 'marks the container as writable' do
+ container.set_repository_read_only!
+
storage_move.do_fail!
expect(container).not_to be_repository_read_only
diff --git a/spec/support/shared_examples/models/diff_positionable_note_shared_examples.rb b/spec/support/shared_examples/models/diff_positionable_note_shared_examples.rb
index eafa589a1d3..a6fcea62aba 100644
--- a/spec/support/shared_examples/models/diff_positionable_note_shared_examples.rb
+++ b/spec/support/shared_examples/models/diff_positionable_note_shared_examples.rb
@@ -48,7 +48,7 @@ RSpec.shared_examples 'a valid diff positionable note' do |factory_on_commit|
end
end
- %i(original_position position change_position).each do |method|
+ %i[original_position position change_position].each do |method|
describe "#{method}=" do
it "doesn't accept non-hash JSON passed as a string" do
subject.send(:"#{method}=", "true")
diff --git a/spec/support/shared_examples/models/project_ci_cd_settings_shared_examples.rb b/spec/support/shared_examples/models/project_ci_cd_settings_shared_examples.rb
index f1af1760e8d..3ecf58168b3 100644
--- a/spec/support/shared_examples/models/project_ci_cd_settings_shared_examples.rb
+++ b/spec/support/shared_examples/models/project_ci_cd_settings_shared_examples.rb
@@ -5,7 +5,7 @@ RSpec.shared_examples 'ci_cd_settings delegation' do
context 'when ci_cd_settings is destroyed but project is not' do
it 'allows methods delegated to ci_cd_settings to be nil', :aggregate_failures do
- attributes = project.ci_cd_settings.attributes.keys - %w(id project_id) - exclude_attributes
+ attributes = project.ci_cd_settings.attributes.keys - %w[id project_id] - exclude_attributes
expect(attributes).to match_array(attributes_with_prefix.keys)
diff --git a/spec/support/shared_examples/models/users/pages_visits_shared_examples.rb b/spec/support/shared_examples/models/users/pages_visits_shared_examples.rb
index 0b3e8516d25..ec7dc1fb8b9 100644
--- a/spec/support/shared_examples/models/users/pages_visits_shared_examples.rb
+++ b/spec/support/shared_examples/models/users/pages_visits_shared_examples.rb
@@ -6,6 +6,10 @@ RSpec.shared_examples 'namespace visits model' do
it { is_expected.to validate_presence_of(:visited_at) }
describe '#visited_around?' do
+ before do
+ described_class.create!(entity_id: entity.id, user_id: user.id, visited_at: base_time)
+ end
+
context 'when the checked time matches a recent visit' do
[-15.minutes, 15.minutes].each do |time_diff|
it 'returns true' do
@@ -24,4 +28,104 @@ RSpec.shared_examples 'namespace visits model' do
end
end
end
+
+ describe '#frecent_visits_scores' do
+ def frecent_visits_scores_to_array(visits)
+ visits.map { |visit| [visit["entity_id"], visit["score"]] }
+ end
+
+ context 'when there is lots of data' do
+ before do
+ create_visit_records
+ end
+
+ it 'returns the frecent items, sorted by their frecency score' do
+ expect(frecent_visits_scores_to_array(described_class.frecent_visits_scores(user_id: user.id,
+ limit: 10))).to eq([[2, 31], [1, 30], [3, 28], [6, 6], [7, 6], [8, 6], [4, 6], [5, 6]])
+ end
+
+ it 'limits the amount of returned entries' do
+ expect(frecent_visits_scores_to_array(described_class.frecent_visits_scores(user_id: user.id,
+ limit: 2))).to eq([
+ [2, 31], [1, 30]
+ ])
+ end
+ end
+
+ context 'when there is few data' do
+ before do
+ [
+ # Multiplier: 4
+ [1, Time.current],
+
+ # Multiplier: 3
+ [2, 2.weeks.ago],
+ [3, 2.weeks.ago],
+
+ # Multiplier: 2
+ [1, 3.weeks.ago],
+ [1, 3.weeks.ago],
+
+ # Multiplier: 1
+ [2, 5.weeks.ago]
+ ].each do |id, datetime|
+ described_class.create!(entity_id: id, user_id: user.id, visited_at: datetime)
+ end
+ end
+
+ it 'returns the frecent items, sorted by their frecency score' do
+ expect(frecent_visits_scores_to_array(described_class.frecent_visits_scores(user_id: user.id,
+ limit: 5))).to eq([
+ [1, 8], # Entity 1 gets a score of (1 * 4) + (2 * 2) = 8
+ [2, 4], # Entity 2 gets a score of (1 * 3) + (1 * 1) = 4
+ [3, 3] # Entity 3 gets a score of 1 * 3 = 3
+ ])
+ end
+ end
+ end
+
+ private
+
+ # rubocop: disable Metrics/AbcSize -- Despite being long, this method is quite straightforward. Splitting it in smaller chunks would likely harm readability more than anything.
+ def create_visit_records
+ [
+ [1, Time.current],
+
+ [2, 1.week.ago],
+ [2, 1.week.ago],
+
+ [2, 2.weeks.ago],
+ [3, 2.weeks.ago],
+ [3, 2.weeks.ago],
+ [4, 2.weeks.ago],
+ [5, 2.weeks.ago],
+ [6, 2.weeks.ago],
+ [7, 2.weeks.ago],
+ [8, 2.weeks.ago],
+
+ [1, 3.weeks.ago],
+ [1, 3.weeks.ago],
+ [3, 3.weeks.ago],
+ [3, 3.weeks.ago],
+
+ [1, 4.weeks.ago],
+ [2, 4.weeks.ago],
+ [2, 4.weeks.ago],
+
+ [3, 7.weeks.ago],
+ [3, 7.weeks.ago],
+
+ [1, 8.weeks.ago],
+ [1, 8.weeks.ago],
+ [1, 8.weeks.ago],
+ [1, 8.weeks.ago],
+
+ [2, 9.weeks.ago],
+ [2, 9.weeks.ago],
+ [2, 9.weeks.ago]
+ ].each do |id, datetime|
+ described_class.create!(entity_id: id, user_id: user.id, visited_at: datetime)
+ end
+ end
+ # rubocop: enable Metrics/AbcSize
end
diff --git a/spec/support/shared_examples/models/wiki_shared_examples.rb b/spec/support/shared_examples/models/wiki_shared_examples.rb
index a0187252108..d731eec5680 100644
--- a/spec/support/shared_examples/models/wiki_shared_examples.rb
+++ b/spec/support/shared_examples/models/wiki_shared_examples.rb
@@ -14,7 +14,7 @@ RSpec.shared_examples 'wiki model' do
subject { wiki }
it 'VALID_USER_MARKUPS contains all valid markups' do
- expect(described_class::VALID_USER_MARKUPS.keys).to match_array(%i(markdown rdoc asciidoc org))
+ expect(described_class::VALID_USER_MARKUPS.keys).to match_array(%i[markdown rdoc asciidoc org])
end
it 'container class includes HasWiki' do
diff --git a/spec/support/shared_examples/path_extraction_shared_examples.rb b/spec/support/shared_examples/path_extraction_shared_examples.rb
index d76348aa26a..5ceaf182d03 100644
--- a/spec/support/shared_examples/path_extraction_shared_examples.rb
+++ b/spec/support/shared_examples/path_extraction_shared_examples.rb
@@ -114,12 +114,12 @@ RSpec.shared_examples 'extracts refs' do
it 'extracts a valid commit SHA' do
expect(extract_ref('f4b14494ef6abf3d144c28e4af0c20143383e062/CHANGELOG')).to eq(
- %w(f4b14494ef6abf3d144c28e4af0c20143383e062 CHANGELOG)
+ %w[f4b14494ef6abf3d144c28e4af0c20143383e062 CHANGELOG]
)
end
it 'falls back to a primitive split for an invalid ref' do
- expect(extract_ref('stable/CHANGELOG')).to eq(%w(stable CHANGELOG))
+ expect(extract_ref('stable/CHANGELOG')).to eq(%w[stable CHANGELOG])
end
it 'extracts the longest matching ref' do
diff --git a/spec/support/shared_examples/quick_actions/commit/tag_quick_action_shared_examples.rb b/spec/support/shared_examples/quick_actions/commit/tag_quick_action_shared_examples.rb
index 14b384b149d..78b667cfe56 100644
--- a/spec/support/shared_examples/quick_actions/commit/tag_quick_action_shared_examples.rb
+++ b/spec/support/shared_examples/quick_actions/commit/tag_quick_action_shared_examples.rb
@@ -5,7 +5,7 @@ RSpec.shared_examples 'tag quick action' do
it 'tags this commit' do
add_note("/tag #{tag_name} #{tag_message}")
- expect(page).to have_content %{Tagged this commit to #{tag_name} with "#{tag_message}".}
+ expect(page).to have_content %(Tagged this commit to #{tag_name} with "#{tag_message}".)
expect(page).to have_content "tagged commit #{truncated_commit_sha}"
expect(page).to have_content tag_name
@@ -21,7 +21,7 @@ RSpec.shared_examples 'tag quick action' do
preview_note("/tag #{tag_name} #{tag_message}")
expect(page).not_to have_content '/tag'
- expect(page).to have_content %{Tags this commit to #{tag_name} with "#{tag_message}"}
+ expect(page).to have_content %(Tags this commit to #{tag_name} with "#{tag_message}")
expect(page).to have_content tag_name
end
end
diff --git a/spec/support/shared_examples/quick_actions/issuable/close_quick_action_shared_examples.rb b/spec/support/shared_examples/quick_actions/issuable/close_quick_action_shared_examples.rb
index 7cbaf40721a..71a8e2a15ce 100644
--- a/spec/support/shared_examples/quick_actions/issuable/close_quick_action_shared_examples.rb
+++ b/spec/support/shared_examples/quick_actions/issuable/close_quick_action_shared_examples.rb
@@ -54,7 +54,7 @@ RSpec.shared_examples 'close quick action' do |issuable_type|
expect(issuable).to be_closed
end
- context "when current user cannot close #{issuable_type}" do
+ context "when current user cannot close #{issuable_type}", :js do
before do
guest = create(:user)
project.add_guest(guest)
diff --git a/spec/support/shared_examples/quick_actions/issue/create_merge_request_quick_action_shared_examples.rb b/spec/support/shared_examples/quick_actions/issue/create_merge_request_quick_action_shared_examples.rb
index 9dc39c6cf73..27d850b8522 100644
--- a/spec/support/shared_examples/quick_actions/issue/create_merge_request_quick_action_shared_examples.rb
+++ b/spec/support/shared_examples/quick_actions/issue/create_merge_request_quick_action_shared_examples.rb
@@ -41,7 +41,7 @@ RSpec.shared_examples 'create_merge_request quick action' do
expect(created_mr.source_branch).to eq(issue.to_branch_name)
visit project_merge_request_path(project, created_mr)
- expect(page).to have_content %{Draft: Resolve "#{issue.title}"}
+ expect(page).to have_content %(Draft: Resolve "#{issue.title}")
end
it 'creates a merge request using the given branch name' do
@@ -54,7 +54,7 @@ RSpec.shared_examples 'create_merge_request quick action' do
expect(created_mr.source_branch).to eq(branch_name)
visit project_merge_request_path(project, created_mr)
- expect(page).to have_content %{Draft: Resolve "#{issue.title}"}
+ expect(page).to have_content %(Draft: Resolve "#{issue.title}")
end
end
end
diff --git a/spec/support/shared_examples/redis/redis_shared_examples.rb b/spec/support/shared_examples/redis/redis_shared_examples.rb
index 1270efd4701..f184f678283 100644
--- a/spec/support/shared_examples/redis/redis_shared_examples.rb
+++ b/spec/support/shared_examples/redis/redis_shared_examples.rb
@@ -365,6 +365,21 @@ RSpec.shared_examples "redis_shared_examples" do
end
end
+ describe '#secret_file' do
+ context 'when explicitly specified in config file' do
+ it 'returns the absolute path of specified file inside Rails root' do
+ allow(subject).to receive(:raw_config_hash).and_return({ secret_file: '/etc/gitlab/redis_secret.enc' })
+ expect(subject.send(:secret_file)).to eq('/etc/gitlab/redis_secret.enc')
+ end
+ end
+
+ context 'when not explicitly specified' do
+ it 'returns the default path in the encrypted settings shared directory' do
+ expect(subject.send(:secret_file)).to eq(Rails.root.join("shared/encrypted_settings/redis.yaml.enc").to_s)
+ end
+ end
+ end
+
describe "#parse_client_tls_options" do
let(:dummy_certificate) { OpenSSL::X509::Certificate.new }
let(:dummy_key) { OpenSSL::PKey::RSA.new }
diff --git a/spec/support/shared_examples/requests/api/container_repositories_shared_examples.rb b/spec/support/shared_examples/requests/api/container_repositories_shared_examples.rb
index 3ff52166990..e7fdf143887 100644
--- a/spec/support/shared_examples/requests/api/container_repositories_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/container_repositories_shared_examples.rb
@@ -39,8 +39,8 @@ RSpec.shared_examples 'returns tags for allowed users' do |user_type, scope|
let(:url) { "/#{scope}s/#{object.id}/registry/repositories?tags=true" }
before do
- stub_container_registry_tags(repository: root_repository.path, tags: %w(rootA latest), with_manifest: true)
- stub_container_registry_tags(repository: test_repository.path, tags: %w(rootA latest), with_manifest: true)
+ stub_container_registry_tags(repository: root_repository.path, tags: %w[rootA latest], with_manifest: true)
+ stub_container_registry_tags(repository: test_repository.path, tags: %w[rootA latest], with_manifest: true)
end
it 'returns a list of repositories and their tags' do
@@ -64,8 +64,8 @@ RSpec.shared_examples 'returns tags for allowed users' do |user_type, scope|
let(:url) { "/#{scope}s/#{object.id}/registry/repositories?tags_count=true" }
before do
- stub_container_registry_tags(repository: root_repository.path, tags: %w(rootA latest), with_manifest: true)
- stub_container_registry_tags(repository: test_repository.path, tags: %w(rootA latest), with_manifest: true)
+ stub_container_registry_tags(repository: root_repository.path, tags: %w[rootA latest], with_manifest: true)
+ stub_container_registry_tags(repository: test_repository.path, tags: %w[rootA latest], with_manifest: true)
end
it 'returns a list of repositories and their tags_count' do
diff --git a/spec/support/shared_examples/requests/api/graphql/issue_list_shared_examples.rb b/spec/support/shared_examples/requests/api/graphql/issue_list_shared_examples.rb
index c6e4aba6968..90fa0c98376 100644
--- a/spec/support/shared_examples/requests/api/graphql/issue_list_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/graphql/issue_list_shared_examples.rb
@@ -5,7 +5,7 @@ RSpec.shared_examples 'graphql issue list request spec' do
let(:fields) do
<<~QUERY
nodes {
- #{all_graphql_fields_for('issues'.classify, excluded: ['relatedMergeRequests'])}
+ #{all_graphql_fields_for('issues'.classify, excluded: %w[relatedMergeRequests productAnalyticsState])}
}
QUERY
end
@@ -26,6 +26,18 @@ RSpec.shared_examples 'graphql issue list request spec' do
issue_b.assignee_ids = another_user.id
end
+ context 'when filtering by state' do
+ context 'when filtering by locked state' do
+ let(:issue_filter_params) { { state: :locked } }
+
+ it 'returns an error message' do
+ post_query
+
+ expect_graphql_errors_to_include(Types::IssuableStateEnum::INVALID_LOCKED_MESSAGE)
+ end
+ end
+ end
+
context 'when filtering by assignees' do
context 'when both assignee_username filters are provided' do
let(:issue_filter_params) do
diff --git a/spec/support/shared_examples/requests/api/graphql/remote_development_shared_examples.rb b/spec/support/shared_examples/requests/api/graphql/remote_development_shared_examples.rb
deleted file mode 100644
index 83e22945361..00000000000
--- a/spec/support/shared_examples/requests/api/graphql/remote_development_shared_examples.rb
+++ /dev/null
@@ -1,48 +0,0 @@
-# frozen_string_literal: true
-
-RSpec.shared_examples 'workspaces query in licensed environment and with feature flag on' do
- describe 'when licensed and remote_development_feature_flag feature flag is enabled' do
- before do
- stub_licensed_features(remote_development: true)
-
- post_graphql(query, current_user: current_user)
- end
-
- it_behaves_like 'a working graphql query'
-
- it { is_expected.to match_array(a_hash_including('name' => workspace.name)) }
-
- context 'when user is not authorized' do
- let(:current_user) { create(:user) }
-
- it { is_expected.to eq([]) }
- end
- end
-end
-
-RSpec.shared_examples 'workspaces query in unlicensed environment and with feature flag off' do
- describe 'when remote_development feature is unlicensed' do
- before do
- stub_licensed_features(remote_development: false)
- post_graphql(query, current_user: current_user)
- end
-
- it 'returns an error' do
- expect(subject).to be_nil
- expect_graphql_errors_to_include(/'remote_development' licensed feature is not available/)
- end
- end
-
- describe 'when remote_development_feature_flag feature flag is disabled' do
- before do
- stub_licensed_features(remote_development: true)
- stub_feature_flags(remote_development_feature_flag: false)
- post_graphql(query, current_user: current_user)
- end
-
- it 'returns an error' do
- expect(subject).to be_nil
- expect_graphql_errors_to_include(/'remote_development_feature_flag' feature flag is disabled/)
- end
- end
-end
diff --git a/spec/support/shared_examples/requests/api/graphql/work_item_list_shared_examples.rb b/spec/support/shared_examples/requests/api/graphql/work_item_list_shared_examples.rb
index a9c422c8f2d..82f98b883dc 100644
--- a/spec/support/shared_examples/requests/api/graphql/work_item_list_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/graphql/work_item_list_shared_examples.rb
@@ -46,6 +46,14 @@ RSpec.shared_examples 'graphql work item list request spec' do
expect(work_item_ids).to include(closed_work_item.to_global_id.to_s)
end
end
+
+ context 'when filtering by state locked' do
+ let(:item_filter_params) { { state: :locked } }
+
+ it 'return an error message' do
+ expect_graphql_errors_to_include(Types::IssuableStateEnum::INVALID_LOCKED_MESSAGE)
+ end
+ end
end
context 'when filtering by type' do
diff --git a/spec/support/shared_examples/requests/api/helm_packages_shared_examples.rb b/spec/support/shared_examples/requests/api/helm_packages_shared_examples.rb
index 7803f0ff04d..9c20b95eb80 100644
--- a/spec/support/shared_examples/requests/api/helm_packages_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/helm_packages_shared_examples.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-RSpec.shared_examples 'rejects helm packages access' do |user_type, status|
+RSpec.shared_examples 'rejects helm packages access' do |user_type, status, body|
context "for user type #{user_type}" do
before do
project.send("add_#{user_type}", user) if user_type != :anonymous && user_type != :not_a_member
@@ -15,6 +15,14 @@ RSpec.shared_examples 'rejects helm packages access' do |user_type, status|
expect(response.headers['WWW-Authenticate']).to eq 'Basic realm="GitLab Packages Registry"'
end
end
+
+ if body
+ it 'has the correct body' do
+ subject
+
+ expect(response.body).to eq(body)
+ end
+ end
end
end
diff --git a/spec/support/shared_examples/requests/api/integrations/github_enterprise_jira_dvcs_end_of_life_shared_examples.rb b/spec/support/shared_examples/requests/api/integrations/github_enterprise_jira_dvcs_end_of_life_shared_examples.rb
deleted file mode 100644
index 6799dec7b80..00000000000
--- a/spec/support/shared_examples/requests/api/integrations/github_enterprise_jira_dvcs_end_of_life_shared_examples.rb
+++ /dev/null
@@ -1,23 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.shared_examples 'a GitHub Enterprise Jira DVCS reversible end of life endpoint' do
- it 'is a reachable endpoint' do
- subject
-
- expect(response).not_to have_gitlab_http_status(:not_found)
- end
-
- context 'when the flag is disabled' do
- before do
- stub_feature_flags(jira_dvcs_end_of_life_amnesty: false)
- end
-
- it 'presents as an endpoint that does not exist' do
- subject
-
- expect(response).to have_gitlab_http_status(:not_found)
- end
- end
-end
diff --git a/spec/support/shared_examples/requests/api/ml/mlflow/mlflow_shared_examples.rb b/spec/support/shared_examples/requests/api/ml/mlflow/mlflow_shared_examples.rb
index 00e50b07909..7978f43610d 100644
--- a/spec/support/shared_examples/requests/api/ml/mlflow/mlflow_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/ml/mlflow/mlflow_shared_examples.rb
@@ -74,6 +74,37 @@ RSpec.shared_examples 'MLflow|shared error cases' do
end
end
+RSpec.shared_examples 'MLflow|shared model registry error cases' do
+ context 'when not authenticated' do
+ let(:headers) { {} }
+
+ it "is Unauthorized" do
+ is_expected.to have_gitlab_http_status(:unauthorized)
+ end
+ end
+
+ context 'when user does not have access' do
+ let(:access_token) { tokens[:different_user] }
+
+ it "is Not Found" do
+ is_expected.to have_gitlab_http_status(:not_found)
+ end
+ end
+
+ context 'when model registry is unavailable' do
+ before do
+ allow(Ability).to receive(:allowed?).and_call_original
+ allow(Ability).to receive(:allowed?)
+ .with(current_user, :read_model_registry, project)
+ .and_return(false)
+ end
+
+ it "is Not Found" do
+ is_expected.to have_gitlab_http_status(:not_found)
+ end
+ end
+end
+
RSpec.shared_examples 'MLflow|Bad Request on missing required' do |keys|
keys.each do |key|
context "when \"#{key}\" is missing" do
diff --git a/spec/support/shared_examples/requests/api/multiple_and_scoped_issue_boards_shared_examples.rb b/spec/support/shared_examples/requests/api/multiple_and_scoped_issue_boards_shared_examples.rb
index d749479544d..fa111ca5811 100644
--- a/spec/support/shared_examples/requests/api/multiple_and_scoped_issue_boards_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/multiple_and_scoped_issue_boards_shared_examples.rb
@@ -5,7 +5,6 @@ RSpec.shared_examples 'multiple and scoped issue boards' do |route_definition|
context 'multiple issue boards' do
before do
- stub_feature_flags(apollo_boards: false)
board_parent.add_reporter(user)
stub_licensed_features(multiple_group_issue_boards: true)
end
diff --git a/spec/support/shared_examples/requests/api/nuget_packages_shared_examples.rb b/spec/support/shared_examples/requests/api/nuget_packages_shared_examples.rb
index f8e78c8c9b1..c23d514abfc 100644
--- a/spec/support/shared_examples/requests/api/nuget_packages_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/nuget_packages_shared_examples.rb
@@ -699,6 +699,7 @@ RSpec.shared_examples 'nuget upload endpoint' do |symbol_package: false|
end
context 'when package duplicates are not allowed' do
+ let(:params) { { package: temp_file(file_name, content: File.open(expand_fixture_path('packages/nuget/package.nupkg'))) } }
let(:headers) { basic_auth_header(deploy_token.username, deploy_token.token).merge(workhorse_headers) }
let_it_be(:existing_package) { create(:nuget_package, project: project) }
let_it_be(:metadata) { { package_name: existing_package.name, package_version: existing_package.version } }
@@ -722,14 +723,6 @@ RSpec.shared_examples 'nuget upload endpoint' do |symbol_package: false|
it_behaves_like 'returning response status', :created
end
-
- context 'when nuget_duplicates_option feature flag is disabled' do
- before do
- stub_feature_flags(nuget_duplicates_option: false)
- end
-
- it_behaves_like 'returning response status', :created
- end
end
end
diff --git a/spec/support/shared_examples/requests/api/repository_storage_moves_shared_examples.rb b/spec/support/shared_examples/requests/api/repository_storage_moves_shared_examples.rb
index 3913d29e086..181bab41e09 100644
--- a/spec/support/shared_examples/requests/api/repository_storage_moves_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/repository_storage_moves_shared_examples.rb
@@ -80,56 +80,9 @@ RSpec.shared_examples 'repository_storage_moves API' do |container_type|
end
end
- describe "GET /#{container_type}/:id/repository_storage_moves" do
- let(:container_id) { container.id }
+ shared_examples 'post single container repository storage move' do
let(:url) { "/#{container_type}/#{container_id}/repository_storage_moves" }
-
- it_behaves_like 'get container repository storage move list'
-
- context 'non-existent container' do
- let(:container_id) { non_existing_record_id }
-
- it 'returns not found' do
- get api(url, user, admin_mode: user.admin?)
-
- expect(response).to have_gitlab_http_status(:not_found)
- end
- end
- end
-
- describe "GET /#{container_type}/:id/repository_storage_moves/:repository_storage_move_id" do
let(:container_id) { container.id }
- let(:url) { "/#{container_type}/#{container_id}/repository_storage_moves/#{repository_storage_move_id}" }
-
- it_behaves_like 'get single container repository storage move'
-
- context 'non-existent container' do
- let(:container_id) { non_existing_record_id }
- let(:repository_storage_move_id) { storage_move.id }
-
- it 'returns not found' do
- get api(url, user, admin_mode: user.admin?)
-
- expect(response).to have_gitlab_http_status(:not_found)
- end
- end
- end
-
- describe "GET /#{container_type.singularize}_repository_storage_moves" do
- it_behaves_like 'get container repository storage move list' do
- let(:url) { "/#{container_type.singularize}_repository_storage_moves" }
- end
- end
-
- describe "GET /#{container_type.singularize}_repository_storage_moves/:repository_storage_move_id" do
- it_behaves_like 'get single container repository storage move' do
- let(:url) { "/#{container_type.singularize}_repository_storage_moves/#{repository_storage_move_id}" }
- end
- end
-
- describe "POST /#{container_type}/:id/repository_storage_moves", :aggregate_failures do
- let(:container_id) { container.id }
- let(:url) { "/#{container_type}/#{container_id}/repository_storage_moves" }
let(:destination_storage_name) { 'test_second_storage' }
def create_container_repository_storage_move
@@ -186,6 +139,57 @@ RSpec.shared_examples 'repository_storage_moves API' do |container_type|
end
end
+ describe "GET /#{container_type}/:id/repository_storage_moves" do
+ let(:container_id) { container.id }
+ let(:url) { "/#{container_type}/#{container_id}/repository_storage_moves" }
+
+ it_behaves_like 'get container repository storage move list'
+
+ context 'non-existent container' do
+ let(:container_id) { non_existing_record_id }
+
+ it 'returns not found' do
+ get api(url, user, admin_mode: user.admin?)
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+ end
+
+ describe "GET /#{container_type}/:id/repository_storage_moves/:repository_storage_move_id" do
+ let(:container_id) { container.id }
+ let(:url) { "/#{container_type}/#{container_id}/repository_storage_moves/#{repository_storage_move_id}" }
+
+ it_behaves_like 'get single container repository storage move'
+
+ context 'non-existent container' do
+ let(:container_id) { non_existing_record_id }
+ let(:repository_storage_move_id) { storage_move.id }
+
+ it 'returns not found' do
+ get api(url, user, admin_mode: user.admin?)
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+ end
+
+ describe "GET /#{container_type.singularize}_repository_storage_moves" do
+ it_behaves_like 'get container repository storage move list' do
+ let(:url) { "/#{container_type.singularize}_repository_storage_moves" }
+ end
+ end
+
+ describe "GET /#{container_type.singularize}_repository_storage_moves/:repository_storage_move_id" do
+ it_behaves_like 'get single container repository storage move' do
+ let(:url) { "/#{container_type.singularize}_repository_storage_moves/#{repository_storage_move_id}" }
+ end
+ end
+
+ describe "POST /#{container_type}/:id/repository_storage_moves", :aggregate_failures do
+ it_behaves_like 'post single container repository storage move'
+ end
+
describe "POST /#{container_type.singularize}_repository_storage_moves" do
let(:url) { "/#{container_type.singularize}_repository_storage_moves" }
let(:source_storage_name) { 'default' }
diff --git a/spec/support/shared_examples/requests/api/rubygems_packages_shared_examples.rb b/spec/support/shared_examples/requests/api/rubygems_packages_shared_examples.rb
index da09d70c777..d7077180b91 100644
--- a/spec/support/shared_examples/requests/api/rubygems_packages_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/rubygems_packages_shared_examples.rb
@@ -151,7 +151,7 @@ RSpec.shared_examples 'dependency endpoint success' do |user_type, status, add_m
context 'with gems params' do
let(:params) { { gems: 'foo,bar' } }
- let(:expected_response) { Marshal.dump(%w(result result)) }
+ let(:expected_response) { Marshal.dump(%w[result result]) }
it 'returns successfully', :aggregate_failures do
service_result = double('DependencyResolverService', execute: ServiceResponse.success(payload: 'result'))
diff --git a/spec/support/shared_examples/sends_git_audit_streaming_event_shared_examples.rb b/spec/support/shared_examples/sends_git_audit_streaming_event_shared_examples.rb
index 2c2be0152a0..f91cf22f27e 100644
--- a/spec/support/shared_examples/sends_git_audit_streaming_event_shared_examples.rb
+++ b/spec/support/shared_examples/sends_git_audit_streaming_event_shared_examples.rb
@@ -14,7 +14,7 @@ RSpec.shared_examples 'sends git audit streaming event' do
let(:project) { create(:project, :public, :repository, namespace: group) }
before do
- group.external_audit_event_destinations.create!(destination_url: 'http://example.com')
+ create(:external_audit_event_destination, group: group)
project.add_developer(user)
end
@@ -38,7 +38,7 @@ RSpec.shared_examples 'sends git audit streaming event' do
let(:project) { create(:project, :private, :repository, namespace: group) }
before do
- group.external_audit_event_destinations.create!(destination_url: 'http://example.com')
+ create(:external_audit_event_destination, group: group)
project.add_developer(user)
sign_in(user)
end
@@ -52,9 +52,40 @@ RSpec.shared_examples 'sends git audit streaming event' do
request.headers.merge! auth_env(user.username, password, nil)
end
end
- it 'sends the audit streaming event' do
- expect(AuditEvents::AuditEventStreamingWorker).to receive(:perform_async).once
- subject
+
+ context 'when log_git_streaming_audit_events is enable' do
+ it 'does not send the audit streaming event' do
+ expect(AuditEvents::AuditEventStreamingWorker).not_to receive(:perform_async)
+ subject
+ end
+
+ it 'respond the need audit to be true' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:ok)
+
+ audit_flag = json_response["need_audit"] || json_response["NeedAudit"]
+ expect(audit_flag).to be_truthy
+ end
+ end
+
+ context 'when log_git_streaming_audit_events is disable' do
+ before do
+ stub_feature_flags(log_git_streaming_audit_events: false)
+ end
+
+ it "sends git streaming audit event" do
+ expect(AuditEvents::AuditEventStreamingWorker).to receive(:perform_async).once
+
+ subject
+ end
+
+ it 'respond the need audit to be false' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response["need_audit"]).to be_falsy
+ end
end
end
end
diff --git a/spec/support/shared_examples/serializers/environment_serializer_shared_examples.rb b/spec/support/shared_examples/serializers/environment_serializer_shared_examples.rb
index 9eaad541df7..b7247f1f243 100644
--- a/spec/support/shared_examples/serializers/environment_serializer_shared_examples.rb
+++ b/spec/support/shared_examples/serializers/environment_serializer_shared_examples.rb
@@ -48,7 +48,7 @@ RSpec.shared_examples 'avoid N+1 on environments serialization' do
query ||= { page: 1, per_page: 20 }
request = double(url: "#{Gitlab.config.gitlab.url}:8080/api/v4/projects?#{query.to_query}", query_parameters: query)
- EnvironmentSerializer.new(current_user: user, project: project).yield_self do |serializer|
+ EnvironmentSerializer.new(current_user: user, project: project).then do |serializer|
serializer.within_folders if grouping
serializer.with_pagination(request, spy('response'))
serializer.represent(Environment.where(project: project))
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 6abf8b242f1..f6be45b0cf8 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
@@ -127,7 +127,7 @@ end
RSpec.shared_examples 'a pullable and pushable' do
it_behaves_like 'an accessible' do
- let(:actions) { %w(pull push) }
+ let(:actions) { %w[pull push] }
end
end
diff --git a/spec/support/shared_examples/services/notification_service_shared_examples.rb b/spec/support/shared_examples/services/notification_service_shared_examples.rb
index df1ae67a590..c53872ca4bc 100644
--- a/spec/support/shared_examples/services/notification_service_shared_examples.rb
+++ b/spec/support/shared_examples/services/notification_service_shared_examples.rb
@@ -45,7 +45,7 @@ RSpec.shared_examples 'group emails are disabled' do
before do
reset_delivered_emails!
- target_group.clear_memoization(:emails_disabled_memoized)
+ target_group.clear_memoization(:emails_enabled_memoized)
end
it 'sends no emails with group emails disabled' do
diff --git a/spec/support/shared_examples/services/protected_branches_shared_examples.rb b/spec/support/shared_examples/services/protected_branches_shared_examples.rb
index 15c63865720..80e2f09ed44 100644
--- a/spec/support/shared_examples/services/protected_branches_shared_examples.rb
+++ b/spec/support/shared_examples/services/protected_branches_shared_examples.rb
@@ -23,3 +23,34 @@ RSpec.shared_context 'with scan result policy blocking protected branches' do
stub_licensed_features(security_orchestration_policies: true)
end
end
+
+RSpec.shared_context 'with scan result policy preventing force pushing' do
+ include RepoHelpers
+
+ let(:policy_path) { Security::OrchestrationPolicyConfiguration::POLICY_PATH }
+ let(:default_branch) { policy_project.default_branch }
+ let(:prevent_pushing_and_force_pushing) { true }
+
+ let(:scan_result_policy) do
+ build(:scan_result_policy, branches: [branch_name],
+ approval_settings: { prevent_pushing_and_force_pushing: prevent_pushing_and_force_pushing })
+ end
+
+ let(:policy_yaml) do
+ build(:orchestration_policy_yaml, scan_result_policy: [scan_result_policy])
+ end
+
+ before do
+ create_file_in_repo(policy_project, default_branch, default_branch, policy_path, policy_yaml)
+ stub_licensed_features(security_orchestration_policies: true)
+ end
+
+ after do
+ policy_project.repository.delete_file(
+ policy_project.creator,
+ policy_path,
+ message: 'Automatically deleted policy',
+ branch_name: default_branch
+ )
+ end
+end
diff --git a/spec/support/shared_examples/validators/url_validator_shared_examples.rb b/spec/support/shared_examples/validators/url_validator_shared_examples.rb
index c5a775fefb6..8547845d0c3 100644
--- a/spec/support/shared_examples/validators/url_validator_shared_examples.rb
+++ b/spec/support/shared_examples/validators/url_validator_shared_examples.rb
@@ -24,7 +24,7 @@ RSpec.shared_examples 'url validator examples' do |schemes|
end
context 'with schemes' do
- let(:options) { { schemes: %w(http) } }
+ let(:options) { { schemes: %w[http] } }
it 'allows urls with the defined schemes' do
subject
diff --git a/spec/support/shared_examples/views/themed_layout_examples.rb b/spec/support/shared_examples/views/themed_layout_examples.rb
index 599fd141dd7..ffbc9026240 100644
--- a/spec/support/shared_examples/views/themed_layout_examples.rb
+++ b/spec/support/shared_examples/views/themed_layout_examples.rb
@@ -8,7 +8,7 @@ RSpec.shared_examples "a layout which reflects the application theme setting" do
it 'renders with the default theme' do
render
- expect(rendered).to have_selector("body.#{default_theme_class}")
+ expect(rendered).to have_selector("html.#{default_theme_class}")
end
end
@@ -24,10 +24,10 @@ RSpec.shared_examples "a layout which reflects the application theme setting" do
render
if chosen_theme.css_class != default_theme_class
- expect(rendered).not_to have_selector("body.#{default_theme_class}")
+ expect(rendered).not_to have_selector("html.#{default_theme_class}")
end
- expect(rendered).to have_selector("body.#{chosen_theme.css_class}")
+ expect(rendered).to have_selector("html.#{chosen_theme.css_class}")
end
end
end
diff --git a/spec/support/sidekiq_middleware.rb b/spec/support/sidekiq_middleware.rb
index 73f43487d7c..f4d90ff5151 100644
--- a/spec/support/sidekiq_middleware.rb
+++ b/spec/support/sidekiq_middleware.rb
@@ -7,7 +7,7 @@ module SidekiqMiddleware
def with_sidekiq_server_middleware(&block)
Sidekiq::Testing.server_middleware.clear
- if Gem::Version.new(Sidekiq::VERSION) != Gem::Version.new('6.5.7')
+ if Gem::Version.new(Sidekiq::VERSION) != Gem::Version.new('6.5.12')
raise 'New version of sidekiq detected, please remove this line'
end
diff --git a/spec/support_specs/graphql/arguments_spec.rb b/spec/support_specs/graphql/arguments_spec.rb
index 925f8c15d68..d96d6985687 100644
--- a/spec/support_specs/graphql/arguments_spec.rb
+++ b/spec/support_specs/graphql/arguments_spec.rb
@@ -50,7 +50,7 @@ RSpec.describe Graphql::Arguments do
hash: { a: 1, b: 2, c: 3 },
int: 42,
float: 2.7,
- string: %q[he said "no"],
+ string: %q(he said "no"),
enum: :OFF,
null: nil,
bool_true: true,
diff --git a/spec/support_specs/helpers/active_record/query_recorder_spec.rb b/spec/support_specs/helpers/active_record/query_recorder_spec.rb
index d6c52b22449..5df88ca8209 100644
--- a/spec/support_specs/helpers/active_record/query_recorder_spec.rb
+++ b/spec/support_specs/helpers/active_record/query_recorder_spec.rb
@@ -47,7 +47,7 @@ RSpec.describe ActiveRecord::QueryRecorder do
query_a = start_with(%q[QueryRecorder SQL: --> SELECT COUNT(*) FROM "schema_migrations"])
- query_b = start_with(%q[QueryRecorder SQL: --> SELECT "schema_migrations".* FROM "schema_migrations" ORDER BY "schema_migrations"."version" ASC LIMIT 1])
+ query_b = start_with(%q(QueryRecorder SQL: --> SELECT "schema_migrations".* FROM "schema_migrations" ORDER BY "schema_migrations"."version" ASC LIMIT 1))
query_c_a = eq(%q[QueryRecorder SQL: --> SELECT "schema_migrations".* FROM "schema_migrations" WHERE (version = 'foo'])
query_c_b = eq(%q(QueryRecorder SQL: --> OR))
diff --git a/spec/support_specs/helpers/migrations_helpers_spec.rb b/spec/support_specs/helpers/migrations_helpers_spec.rb
index 2af16151350..725caef7a63 100644
--- a/spec/support_specs/helpers/migrations_helpers_spec.rb
+++ b/spec/support_specs/helpers/migrations_helpers_spec.rb
@@ -55,7 +55,7 @@ RSpec.describe MigrationsHelpers, feature_category: :database do
end
it 'create a class based on the CI base model' do
- klass = helper.table(:ci_builds, database: :ci)
+ klass = helper.table(:p_ci_builds, database: :ci) { |model| model.primary_key = :id }
expect(klass.connection_specification_name).to eq('Ci::ApplicationRecord')
end
end
@@ -66,7 +66,7 @@ RSpec.describe MigrationsHelpers, feature_category: :database do
end
it 'creates a class based on main base model' do
- klass = helper.table(:ci_builds, database: :ci)
+ klass = helper.table(:p_ci_builds, database: :ci) { |model| model.primary_key = :id }
expect(klass.connection_specification_name).to eq('ActiveRecord::Base')
end
end
diff --git a/spec/support_specs/helpers/stub_saas_features_spec.rb b/spec/support_specs/helpers/stub_saas_features_spec.rb
index ed973071a6d..c3cec3f47aa 100644
--- a/spec/support_specs/helpers/stub_saas_features_spec.rb
+++ b/spec/support_specs/helpers/stub_saas_features_spec.rb
@@ -6,7 +6,7 @@ RSpec.describe StubSaasFeatures, feature_category: :shared do
describe '#stub_saas_features' do
using RSpec::Parameterized::TableSyntax
- let(:feature_name) { '_some_saas_feature_' }
+ let(:feature_name) { :some_saas_feature }
context 'when checking global state' do
where(:feature_value) do
@@ -41,10 +41,10 @@ RSpec.describe StubSaasFeatures, feature_category: :shared do
end
it 'handles multiple features' do
- stub_saas_features(feature_name => false, '_some_new_feature_' => true)
+ stub_saas_features(feature_name => false, some_new_feature: true)
expect(::Gitlab::Saas.feature_available?(feature_name)).to eq(false)
- expect(::Gitlab::Saas.feature_available?('_some_new_feature_')).to eq(true)
+ expect(::Gitlab::Saas.feature_available?(:some_new_feature)).to eq(true)
end
end
end
diff --git a/spec/support_specs/matchers/exceed_query_limit_helpers_spec.rb b/spec/support_specs/matchers/exceed_query_limit_helpers_spec.rb
index 19581064626..7c957586b10 100644
--- a/spec/support_specs/matchers/exceed_query_limit_helpers_spec.rb
+++ b/spec/support_specs/matchers/exceed_query_limit_helpers_spec.rb
@@ -147,17 +147,17 @@ RSpec.describe ExceedQueryLimitHelpers do
test_matcher = TestMatcher.new
recorder = ActiveRecord::QueryRecorder.new do
- TestQueries.find_by(version: %w(foo bar baz).join("\n"))
- TestQueries.find_by(version: %w(foo biz baz).join("\n"))
- TestQueries.find_by(version: %w(foo bar baz).join("\n"))
+ TestQueries.find_by(version: %w[foo bar baz].join("\n"))
+ TestQueries.find_by(version: %w[foo biz baz].join("\n"))
+ TestQueries.find_by(version: %w[foo bar baz].join("\n"))
end
recorder.count
expect(test_matcher.count_queries(recorder)).to eq({
'SELECT "schema_migrations".* FROM "schema_migrations"' => {
- %[WHERE "schema_migrations"."version" = 'foo\nbar\nbaz' LIMIT 1] => 2,
- %[WHERE "schema_migrations"."version" = 'foo\nbiz\nbaz' LIMIT 1] => 1
+ %(WHERE "schema_migrations"."version" = 'foo\nbar\nbaz' LIMIT 1) => 2,
+ %(WHERE "schema_migrations"."version" = 'foo\nbiz\nbaz' LIMIT 1) => 1
}
})
end
@@ -183,13 +183,13 @@ RSpec.describe ExceedQueryLimitHelpers do
expect(test_matcher.count_queries(recorder)).to eq({
'SELECT "schema_migrations".* FROM "schema_migrations"' => {
- %q[WHERE "schema_migrations"."version" = 'foobar'] => 2,
- %q[WHERE "schema_migrations"."version" = 'also foobar and baz'] => 1,
- %q[ORDER BY "schema_migrations"."version" ASC LIMIT 1] => 1
+ %q(WHERE "schema_migrations"."version" = 'foobar') => 2,
+ %q(WHERE "schema_migrations"."version" = 'also foobar and baz') => 1,
+ %q(ORDER BY "schema_migrations"."version" ASC LIMIT 1) => 1
},
'SELECT COUNT(*) FROM "schema_migrations"' => {
"" => 2,
- %q[WHERE "schema_migrations"."version" = 'foobar'] => 1
+ %q(WHERE "schema_migrations"."version" = 'foobar') => 1
},
'SAVEPOINT active_record_1' => { "" => 1 },
'INSERT INTO "schema_migrations" ("version")' => {
@@ -197,11 +197,11 @@ RSpec.describe ExceedQueryLimitHelpers do
},
'RELEASE SAVEPOINT active_record_1' => { "" => 1 },
'UPDATE "schema_migrations"' => {
- %q[SET "version" = 'y' WHERE "schema_migrations"."version" = 'x'] => 1,
- %q[SET "version" = 'z' WHERE "schema_migrations"."version" = 'y'] => 1
+ %q(SET "version" = 'y' WHERE "schema_migrations"."version" = 'x') => 1,
+ %q(SET "version" = 'z' WHERE "schema_migrations"."version" = 'y') => 1
},
'DELETE FROM "schema_migrations"' => {
- %q[WHERE "schema_migrations"."version" = 'z'] => 1
+ %q(WHERE "schema_migrations"."version" = 'z') => 1
}
})
end
diff --git a/spec/tasks/gitlab/background_migrations_rake_spec.rb b/spec/tasks/gitlab/background_migrations_rake_spec.rb
index ba5618e2700..5c61f0eff38 100644
--- a/spec/tasks/gitlab/background_migrations_rake_spec.rb
+++ b/spec/tasks/gitlab/background_migrations_rake_spec.rb
@@ -149,6 +149,7 @@ RSpec.describe 'gitlab:background_migrations namespace rake tasks', :suppress_gi
context 'with two connections sharing the same database' do
before do
skip_if_database_exists(:ci)
+ skip_if_database_exists(:jh)
end
it 'skips the shared database' do
diff --git a/spec/tasks/gitlab/backup_rake_spec.rb b/spec/tasks/gitlab/backup_rake_spec.rb
index 56560b06219..75be7b97a67 100644
--- a/spec/tasks/gitlab/backup_rake_spec.rb
+++ b/spec/tasks/gitlab/backup_rake_spec.rb
@@ -234,9 +234,7 @@ RSpec.describe 'gitlab:backup namespace rake tasks', :delete, feature_category:
raw_repo = excluded_project.repository.raw
- # The restore will not find the repository in the backup, but will create
- # an empty one in its place
- expect(raw_repo.empty?).to be(true)
+ expect(raw_repo).not_to exist
end
end
diff --git a/spec/tasks/gitlab/click_house/migration_rake_spec.rb b/spec/tasks/gitlab/click_house/migration_rake_spec.rb
new file mode 100644
index 00000000000..75a1c1a1856
--- /dev/null
+++ b/spec/tasks/gitlab/click_house/migration_rake_spec.rb
@@ -0,0 +1,133 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'gitlab:clickhouse', click_house: :without_migrations, feature_category: :database do
+ include ClickHouseTestHelpers
+
+ # We don't need to delete data since we don't modify Postgres data
+ self.use_transactional_tests = false
+
+ let(:migrations_base_dir) { 'click_house/migrations' }
+ let(:migrations_dirname) { '' }
+ let(:migrations_dir) { expand_fixture_path("#{migrations_base_dir}/#{migrations_dirname}") }
+ let(:verbose) { nil }
+
+ before(:all) do
+ Rake.application.rake_require 'tasks/gitlab/click_house/migration'
+ end
+
+ before do
+ stub_env('VERBOSE', verbose) if verbose
+ end
+
+ describe 'migrate' do
+ subject(:migration) { run_rake_task('gitlab:clickhouse:migrate') }
+
+ let(:target_version) { nil }
+
+ around do |example|
+ ClickHouse::MigrationSupport::Migrator.migrations_paths = [migrations_dir]
+
+ example.run
+
+ clear_consts(expand_fixture_path(migrations_base_dir))
+ end
+
+ before do
+ stub_env('VERSION', target_version) if target_version
+ end
+
+ describe 'when creating a table' do
+ let(:migrations_dirname) { 'plain_table_creation' }
+
+ it 'creates a table' do
+ expect { migration }.to change { active_schema_migrations_count }.from(0).to(1)
+ .and output.to_stdout
+
+ expect(describe_table('some')).to match({
+ id: a_hash_including(type: 'UInt64'),
+ date: a_hash_including(type: 'Date')
+ })
+ end
+
+ context 'when VERBOSE is false' do
+ let(:verbose) { 'false' }
+
+ it 'does not write to stdout' do
+ expect { migration }.not_to output.to_stdout
+
+ expect(describe_table('some')).to match({
+ id: a_hash_including(type: 'UInt64'),
+ date: a_hash_including(type: 'Date')
+ })
+ end
+ end
+ end
+
+ describe 'when dropping a table' do
+ let(:migrations_dirname) { 'drop_table' }
+ let(:target_version) { 2 }
+
+ it 'drops table' do
+ stub_env('VERSION', 1)
+ run_rake_task('gitlab:clickhouse:migrate')
+
+ expect(table_names).to include('some')
+
+ stub_env('VERSION', target_version)
+ migration
+ expect(table_names).not_to include('some')
+ end
+ end
+
+ describe 'with VERSION is invalid' do
+ let(:migrations_dirname) { 'plain_table_creation' }
+ let(:target_version) { 'invalid' }
+
+ it { expect { migration }.to raise_error RuntimeError, 'Invalid format of target version: `VERSION=invalid`' }
+ end
+ end
+
+ describe 'rollback' do
+ subject(:migration) { run_rake_task('gitlab:clickhouse:rollback') }
+
+ let(:schema_migration) { ClickHouse::MigrationSupport::SchemaMigration }
+
+ around do |example|
+ ClickHouse::MigrationSupport::Migrator.migrations_paths = [migrations_dir]
+ migrate(nil, ClickHouse::MigrationSupport::MigrationContext.new(migrations_dir, schema_migration))
+
+ example.run
+
+ clear_consts(expand_fixture_path(migrations_base_dir))
+ end
+
+ context 'when migrating back all the way to 0' do
+ let(:target_version) { 0 }
+
+ context 'when down method is present' do
+ let(:migrations_dirname) { 'table_creation_with_down_method' }
+
+ it 'removes migration' do
+ expect(table_names).to include('some')
+
+ migration
+ expect(table_names).not_to include('some')
+ end
+ end
+ end
+ end
+
+ %w[gitlab:clickhouse:migrate].each do |task|
+ context "when running #{task}" do
+ it "does run gitlab:clickhouse:prepare_schema_migration_table before" do
+ expect(Rake::Task['gitlab:clickhouse:prepare_schema_migration_table']).to receive(:execute).and_return(true)
+ expect(Rake::Task[task]).to receive(:execute).and_return(true)
+
+ Rake::Task['gitlab:clickhouse:prepare_schema_migration_table'].reenable
+ run_rake_task(task)
+ end
+ end
+ end
+end
diff --git a/spec/tasks/gitlab/db_rake_spec.rb b/spec/tasks/gitlab/db_rake_spec.rb
index c2e53da8d4b..a966f2118b0 100644
--- a/spec/tasks/gitlab/db_rake_spec.rb
+++ b/spec/tasks/gitlab/db_rake_spec.rb
@@ -7,6 +7,7 @@ RSpec.describe 'gitlab:db namespace rake task', :silence_stdout, feature_categor
Rake.application.rake_require 'active_record/railties/databases'
Rake.application.rake_require 'tasks/seed_fu'
Rake.application.rake_require 'tasks/gitlab/db'
+ Rake.application.rake_require 'tasks/gitlab/db/lock_writes'
end
before do
@@ -14,6 +15,7 @@ RSpec.describe 'gitlab:db namespace rake task', :silence_stdout, feature_categor
allow(Rake::Task['db:migrate']).to receive(:invoke).and_return(true)
allow(Rake::Task['db:schema:load']).to receive(:invoke).and_return(true)
allow(Rake::Task['db:seed_fu']).to receive(:invoke).and_return(true)
+ allow(Rake::Task['gitlab:db:lock_writes']).to receive(:invoke).and_return(true)
stub_feature_flags(disallow_database_ddl_feature_flags: false)
end
@@ -142,6 +144,7 @@ RSpec.describe 'gitlab:db namespace rake task', :silence_stdout, feature_categor
expect(Rake::Task['db:migrate']).to receive(:invoke)
expect(Rake::Task['db:schema:load']).not_to receive(:invoke)
+ expect(Rake::Task['gitlab:db:lock_writes']).not_to receive(:invoke)
expect(Rake::Task['db:seed_fu']).not_to receive(:invoke)
run_rake_task('gitlab:db:configure')
@@ -153,6 +156,7 @@ RSpec.describe 'gitlab:db namespace rake task', :silence_stdout, feature_categor
allow(connection).to receive(:tables).and_return([])
expect(Rake::Task['db:schema:load']).to receive(:invoke)
+ expect(Rake::Task['gitlab:db:lock_writes']).to receive(:invoke)
expect(Rake::Task['db:seed_fu']).to receive(:invoke)
expect(Rake::Task['db:migrate']).not_to receive(:invoke)
@@ -165,6 +169,7 @@ RSpec.describe 'gitlab:db namespace rake task', :silence_stdout, feature_categor
allow(connection).to receive(:tables).and_return(['default'])
expect(Rake::Task['db:schema:load']).to receive(:invoke)
+ expect(Rake::Task['gitlab:db:lock_writes']).to receive(:invoke)
expect(Rake::Task['db:seed_fu']).to receive(:invoke)
expect(Rake::Task['db:migrate']).not_to receive(:invoke)
@@ -177,6 +182,7 @@ RSpec.describe 'gitlab:db namespace rake task', :silence_stdout, feature_categor
allow(connection).to receive(:tables).and_return([])
expect(Rake::Task['db:schema:load']).to receive(:invoke).and_raise('error')
+ expect(Rake::Task['gitlab:db:lock_writes']).not_to receive(:invoke)
expect(Rake::Task['db:seed_fu']).not_to receive(:invoke)
expect(Rake::Task['db:migrate']).not_to receive(:invoke)
@@ -201,6 +207,7 @@ RSpec.describe 'gitlab:db namespace rake task', :silence_stdout, feature_categor
expect(Gitlab::Database).to receive(:add_post_migrate_path_to_rails).and_call_original
expect(Rake::Task['db:schema:load']).to receive(:invoke)
+ expect(Rake::Task['gitlab:db:lock_writes']).to receive(:invoke)
expect(Rake::Task['db:seed_fu']).to receive(:invoke)
expect(Rake::Task['db:migrate']).not_to receive(:invoke)
@@ -217,6 +224,7 @@ RSpec.describe 'gitlab:db namespace rake task', :silence_stdout, feature_categor
expect(Rake::Task['db:migrate']).to receive(:invoke)
expect(Gitlab::Database).not_to receive(:add_post_migrate_path_to_rails)
expect(Rake::Task['db:schema:load']).not_to receive(:invoke)
+ expect(Rake::Task['gitlab:db:lock_writes']).not_to receive(:invoke)
expect(Rake::Task['db:seed_fu']).not_to receive(:invoke)
run_rake_task('gitlab:db:configure')
@@ -280,6 +288,7 @@ RSpec.describe 'gitlab:db namespace rake task', :silence_stdout, feature_categor
expect(Rake::Task['db:migrate:main']).not_to receive(:invoke)
expect(Rake::Task['db:migrate:ci']).not_to receive(:invoke)
+ expect(Rake::Task['gitlab:db:lock_writes']).to receive(:invoke)
expect(Rake::Task['db:seed_fu']).to receive(:invoke)
run_rake_task('gitlab:db:configure')
@@ -299,6 +308,7 @@ RSpec.describe 'gitlab:db namespace rake task', :silence_stdout, feature_categor
expect(Rake::Task['db:schema:load:main']).not_to receive(:invoke)
expect(Rake::Task['db:schema:load:ci']).not_to receive(:invoke)
+ expect(Rake::Task['gitlab:db:lock_writes']).not_to receive(:invoke)
expect(Rake::Task['db:seed_fu']).not_to receive(:invoke)
run_rake_task('gitlab:db:configure')
@@ -569,6 +579,10 @@ RSpec.describe 'gitlab:db namespace rake task', :silence_stdout, feature_categor
allow(File).to receive(:open).with(Rails.root.join('ee/db/geo/structure.sql').to_s, any_args).and_yield(output)
allow(File).to receive(:open).with(Rails.root.join('ee/db/embedding/structure.sql').to_s, any_args).and_yield(output)
end
+
+ if Gitlab.jh?
+ allow(File).to receive(:open).with(Rails.root.join('jh/db/structure.sql').to_s, any_args).and_yield(output)
+ end
end
after do
@@ -587,8 +601,8 @@ RSpec.describe 'gitlab:db namespace rake task', :silence_stdout, feature_categor
end
describe 'drop_tables' do
- let(:tables) { %w(one two schema_migrations) }
- let(:views) { %w(three four pg_stat_statements) }
+ let(:tables) { %w[one two schema_migrations] }
+ let(:views) { %w[three four pg_stat_statements] }
let(:schemas) { Gitlab::Database::EXTRA_SCHEMAS }
let(:ignored_views) { double(ActiveRecord::Relation, pluck: ['pg_stat_statements']) }
@@ -1190,6 +1204,8 @@ RSpec.describe 'gitlab:db namespace rake task', :silence_stdout, feature_categor
let(:config_hash) { { username: 'foo' } }
before do
+ skip_if_shared_database(:ci)
+
allow(Rake::Task['db:drop']).to receive(:invoke)
allow(Rake::Task['db:create']).to receive(:invoke)
allow(ActiveRecord::Base).to receive(:configurations).and_return(configurations)
@@ -1201,7 +1217,7 @@ RSpec.describe 'gitlab:db namespace rake task', :silence_stdout, feature_categor
it 'migrate as nonsuperuser check with default username' do
expect(config_hash).to receive(:merge).with({ username: 'gitlab' }).and_call_original
expect(Gitlab::Database).to receive(:check_for_non_superuser)
- expect(Rake::Task['db:migrate']).to receive(:invoke)
+ expect(Rake::Task['db:migrate:main']).to receive(:invoke)
run_rake_task('gitlab:db:reset_as_non_superuser')
end
@@ -1209,7 +1225,7 @@ RSpec.describe 'gitlab:db namespace rake task', :silence_stdout, feature_categor
it 'migrate as nonsuperuser check with specified username' do
expect(config_hash).to receive(:merge).with({ username: 'foo' }).and_call_original
expect(Gitlab::Database).to receive(:check_for_non_superuser)
- expect(Rake::Task['db:migrate']).to receive(:invoke)
+ expect(Rake::Task['db:migrate:main']).to receive(:invoke)
run_rake_task('gitlab:db:reset_as_non_superuser', '[foo]')
end
diff --git a/spec/tasks/gitlab/feature_categories_rake_spec.rb b/spec/tasks/gitlab/feature_categories_rake_spec.rb
index 84558ea7fb7..1dee72eee46 100644
--- a/spec/tasks/gitlab/feature_categories_rake_spec.rb
+++ b/spec/tasks/gitlab/feature_categories_rake_spec.rb
@@ -10,16 +10,6 @@ RSpec.describe 'gitlab:feature_categories:index', :silence_stdout, feature_categ
it 'outputs objects by stage group' do
# Sample items that _hopefully_ won't change very often.
expected = {
- 'controller_actions' => a_hash_including(
- 'integrations' => a_collection_including(
- klass: 'Oauth::JiraDvcs::AuthorizationsController',
- action: 'new',
- source_location: [
- 'app/controllers/oauth/jira_dvcs/authorizations_controller.rb',
- an_instance_of(Integer)
- ]
- )
- ),
'api_endpoints' => a_hash_including(
'system_access' => a_collection_including(
klass: 'API::AccessRequests',
diff --git a/spec/tasks/gitlab/redis_rake_spec.rb b/spec/tasks/gitlab/redis_rake_spec.rb
new file mode 100644
index 00000000000..bfad25be4fd
--- /dev/null
+++ b/spec/tasks/gitlab/redis_rake_spec.rb
@@ -0,0 +1,188 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'gitlab:redis:secret rake tasks', :silence_stdout, feature_category: :build do
+ let(:redis_secret_file) { 'tmp/tests/redisenc/redis_secret.yaml.enc' }
+
+ before do
+ Rake.application.rake_require 'tasks/gitlab/redis'
+ stub_env('EDITOR', 'cat')
+ stub_warn_user_is_not_gitlab
+ FileUtils.mkdir_p('tmp/tests/redisenc/')
+ allow(::Gitlab::Runtime).to receive(:rake?).and_return(true)
+ allow_next_instance_of(Gitlab::Redis::Cache) do |instance|
+ allow(instance).to receive(:secret_file).and_return(redis_secret_file)
+ end
+ allow(Gitlab::Application.secrets).to receive(:encrypted_settings_key_base).and_return(SecureRandom.hex(64))
+ end
+
+ after do
+ FileUtils.rm_rf(Rails.root.join('tmp/tests/redisenc'))
+ end
+
+ describe ':show' do
+ it 'displays error when file does not exist' do
+ expect do
+ run_rake_task('gitlab:redis:secret:show')
+ end.to output(/File .* does not exist. Use `gitlab-rake gitlab:redis:secret:edit` to change that./).to_stdout
+ end
+
+ it 'displays error when key does not exist' do
+ Settings.encrypted(redis_secret_file).write('somevalue')
+ allow(Gitlab::Application.secrets).to receive(:encrypted_settings_key_base).and_return(nil)
+ expect do
+ run_rake_task('gitlab:redis:secret:show')
+ end.to output(/Missing encryption key encrypted_settings_key_base./).to_stderr
+ end
+
+ it 'displays error when key is changed' do
+ Settings.encrypted(redis_secret_file).write('somevalue')
+ allow(Gitlab::Application.secrets).to receive(:encrypted_settings_key_base).and_return(SecureRandom.hex(64))
+ expect do
+ run_rake_task('gitlab:redis:secret:show')
+ end.to output(/Couldn't decrypt .* Perhaps you passed the wrong key?/).to_stderr
+ end
+
+ it 'outputs the unencrypted content when present' do
+ encrypted = Settings.encrypted(redis_secret_file)
+ encrypted.write('somevalue')
+ expect { run_rake_task('gitlab:redis:secret:show') }.to output(/somevalue/).to_stdout
+ end
+ end
+
+ describe 'edit' do
+ it 'creates encrypted file' do
+ expect { run_rake_task('gitlab:redis:secret:edit') }.to output(/File encrypted and saved./).to_stdout
+ expect(File.exist?(redis_secret_file)).to be true
+ value = Settings.encrypted(redis_secret_file)
+ expect(value.read).to match(/password: '123'/)
+ end
+
+ it 'displays error when key does not exist' do
+ allow(Gitlab::Application.secrets).to receive(:encrypted_settings_key_base).and_return(nil)
+ expect do
+ run_rake_task('gitlab:redis:secret:edit')
+ end.to output(/Missing encryption key encrypted_settings_key_base./).to_stderr
+ end
+
+ it 'displays error when key is changed' do
+ Settings.encrypted(redis_secret_file).write('somevalue')
+ allow(Gitlab::Application.secrets).to receive(:encrypted_settings_key_base).and_return(SecureRandom.hex(64))
+ expect do
+ run_rake_task('gitlab:redis:secret:edit')
+ end.to output(/Couldn't decrypt .* Perhaps you passed the wrong key?/).to_stderr
+ end
+
+ it 'displays error when write directory does not exist' do
+ FileUtils.rm_rf(Rails.root.join('tmp/tests/redisenc'))
+ expect { run_rake_task('gitlab:redis:secret:edit') }.to output(/Directory .* does not exist./).to_stderr
+ end
+
+ it 'shows a warning when content is invalid' do
+ Settings.encrypted(redis_secret_file).write('somevalue')
+ expect do
+ run_rake_task('gitlab:redis:secret:edit')
+ end.to output(/WARNING: Content was not a valid Redis secret yml file/).to_stdout
+ value = Settings.encrypted(redis_secret_file)
+ expect(value.read).to match(/somevalue/)
+ end
+
+ it 'displays error when $EDITOR is not set' do
+ stub_env('EDITOR', nil)
+ expect do
+ run_rake_task('gitlab:redis:secret:edit')
+ end.to output(/No \$EDITOR specified to open file. Please provide one when running the command/).to_stderr
+ end
+ end
+
+ describe 'write' do
+ before do
+ allow($stdin).to receive(:tty?).and_return(false)
+ allow($stdin).to receive(:read).and_return('testvalue')
+ end
+
+ it 'creates encrypted file from stdin' do
+ expect { run_rake_task('gitlab:redis:secret:write') }.to output(/File encrypted and saved./).to_stdout
+ expect(File.exist?(redis_secret_file)).to be true
+ value = Settings.encrypted(redis_secret_file)
+ expect(value.read).to match(/testvalue/)
+ end
+
+ it 'displays error when key does not exist' do
+ allow(Gitlab::Application.secrets).to receive(:encrypted_settings_key_base).and_return(nil)
+ expect do
+ run_rake_task('gitlab:redis:secret:write')
+ end.to output(/Missing encryption key encrypted_settings_key_base./).to_stderr
+ end
+
+ it 'displays error when write directory does not exist' do
+ FileUtils.rm_rf('tmp/tests/redisenc/')
+ expect { run_rake_task('gitlab:redis:secret:write') }.to output(/Directory .* does not exist./).to_stderr
+ end
+
+ it 'shows a warning when content is invalid' do
+ Settings.encrypted(redis_secret_file).write('somevalue')
+ expect do
+ run_rake_task('gitlab:redis:secret:edit')
+ end.to output(/WARNING: Content was not a valid Redis secret yml file/).to_stdout
+ expect(Settings.encrypted(redis_secret_file).read).to match(/somevalue/)
+ end
+ end
+
+ context 'when an instance class is specified' do
+ before do
+ allow_next_instance_of(Gitlab::Redis::SharedState) do |instance|
+ allow(instance).to receive(:secret_file).and_return(redis_secret_file)
+ end
+ end
+
+ context 'when actual name is used' do
+ it 'uses the correct Redis class' do
+ expect(Gitlab::Redis::SharedState).to receive(:encrypted_secrets).and_call_original
+
+ run_rake_task('gitlab:redis:secret:edit', 'SharedState')
+ end
+ end
+
+ context 'when name in lowercase is used' do
+ it 'uses the correct Redis class' do
+ expect(Gitlab::Redis::SharedState).to receive(:encrypted_secrets).and_call_original
+
+ run_rake_task('gitlab:redis:secret:edit', 'sharedstate')
+ end
+ end
+
+ context 'when name with underscores is used' do
+ it 'uses the correct Redis class' do
+ expect(Gitlab::Redis::SharedState).to receive(:encrypted_secrets).and_call_original
+
+ run_rake_task('gitlab:redis:secret:edit', 'shared_state')
+ end
+ end
+
+ context 'when name with hyphens is used' do
+ it 'uses the correct Redis class' do
+ expect(Gitlab::Redis::SharedState).to receive(:encrypted_secrets).and_call_original
+
+ run_rake_task('gitlab:redis:secret:edit', 'shared-state')
+ end
+ end
+
+ context 'when name with spaces is used' do
+ it 'uses the correct Redis class' do
+ expect(Gitlab::Redis::SharedState).to receive(:encrypted_secrets).and_call_original
+
+ run_rake_task('gitlab:redis:secret:edit', 'shared state')
+ end
+ end
+
+ context 'when an invalid name is used' do
+ it 'raises error' do
+ expect do
+ run_rake_task('gitlab:redis:secret:edit', 'foobar')
+ end.to raise_error(/Specified instance name foobar does not exist./)
+ end
+ end
+ end
+end
diff --git a/spec/tooling/danger/analytics_instrumentation_spec.rb b/spec/tooling/danger/analytics_instrumentation_spec.rb
index 5d12647e02f..79c75b2e89c 100644
--- a/spec/tooling/danger/analytics_instrumentation_spec.rb
+++ b/spec/tooling/danger/analytics_instrumentation_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'gitlab-dangerfiles'
+require 'fast_spec_helper'
require 'gitlab/dangerfiles/spec_helper'
require_relative '../../../tooling/danger/analytics_instrumentation'
@@ -231,4 +231,64 @@ RSpec.describe Tooling::Danger::AnalyticsInstrumentation, feature_category: :ser
end
end
end
+
+ describe '#check_deprecated_data_sources!' do
+ let(:fake_project_helper) { instance_double(Tooling::Danger::ProjectHelper) }
+
+ subject(:check_data_source) { analytics_instrumentation.check_deprecated_data_sources! }
+
+ before do
+ allow(fake_helper).to receive(:added_files).and_return([added_file])
+ allow(fake_helper).to receive(:changed_lines).with(added_file).and_return(changed_lines)
+ allow(analytics_instrumentation).to receive(:project_helper).and_return(fake_project_helper)
+ allow(analytics_instrumentation.project_helper).to receive(:file_lines).and_return(changed_lines.map { |line| line.delete_prefix('+') })
+ end
+
+ context 'when no metric definitions were modified' do
+ let(:added_file) { 'app/models/user.rb' }
+ let(:changed_lines) { ['+ data_source: redis,'] }
+
+ it 'does not trigger warning' do
+ expect(analytics_instrumentation).not_to receive(:markdown)
+
+ check_data_source
+ end
+ end
+
+ context 'when metrics fields were modified' do
+ let(:added_file) { 'config/metrics/count7_d/example_metric.yml' }
+
+ [:redis, :redis_hll].each do |source|
+ context "when source is #{source}" do
+ let(:changed_lines) { ["+ data_source: #{source}"] }
+ let(:template) do
+ <<~SUGGEST_COMMENT
+ ```suggestion
+ data_source: internal_events
+ ```
+
+ %<message>s
+ SUGGEST_COMMENT
+ end
+
+ it 'issues a warning' do
+ expected_comment = format(template, message: Tooling::Danger::AnalyticsInstrumentation::CHANGE_DEPRECATED_DATA_SOURCE_MESSAGE)
+ expect(analytics_instrumentation).to receive(:markdown).with(expected_comment.strip, file: added_file, line: 1)
+
+ check_data_source
+ end
+ end
+ end
+
+ context 'when neither redis nor redis_hll used as a data_source' do
+ let(:changed_lines) { ['+ data_source: database,'] }
+
+ it 'does not issue a warning' do
+ expect(analytics_instrumentation).not_to receive(:markdown)
+
+ check_data_source
+ end
+ end
+ end
+ end
end
diff --git a/spec/tooling/danger/bulk_database_actions_spec.rb b/spec/tooling/danger/bulk_database_actions_spec.rb
index 620b4ac2b18..eba3eacb212 100644
--- a/spec/tooling/danger/bulk_database_actions_spec.rb
+++ b/spec/tooling/danger/bulk_database_actions_spec.rb
@@ -1,10 +1,8 @@
# frozen_string_literal: true
-require 'gitlab-dangerfiles'
-require 'danger'
-require 'danger/plugins/internal/helper'
-require 'gitlab/dangerfiles/spec_helper'
+require 'fast_spec_helper'
require 'rspec-parameterized'
+require 'gitlab/dangerfiles/spec_helper'
require_relative '../../../tooling/danger/bulk_database_actions'
require_relative '../../../tooling/danger/project_helper'
diff --git a/spec/tooling/danger/change_column_default_spec.rb b/spec/tooling/danger/change_column_default_spec.rb
new file mode 100644
index 00000000000..8cfcbfa1dc0
--- /dev/null
+++ b/spec/tooling/danger/change_column_default_spec.rb
@@ -0,0 +1,104 @@
+# frozen_string_literal: true
+
+require 'danger'
+require 'gitlab/dangerfiles/spec_helper'
+
+require_relative '../../../tooling/danger/change_column_default'
+require_relative '../../../tooling/danger/project_helper'
+
+RSpec.describe Tooling::Danger::ChangeColumnDefault, feature_category: :tooling do
+ subject(:change_column_default) { fake_danger.new(helper: fake_helper) }
+
+ let(:fake_danger) { DangerSpecHelper.fake_danger.include(described_class) }
+ let(:fake_project_helper) { instance_double(Tooling::Danger::ProjectHelper) }
+ let(:comment) { described_class::COMMENT.chomp }
+ let(:file_diff) do
+ File.read(File.expand_path("../fixtures/change_column_default_migration.txt", __dir__)).split("\n")
+ end
+
+ include_context "with dangerfile"
+
+ describe '#add_comment_for_change_column_default' do
+ let(:file_lines) { file_diff.map { |line| line.delete_prefix('+').delete_prefix('-') } }
+ let(:matching_lines) { [7, 9, 11] }
+
+ before do
+ allow(change_column_default).to receive(:project_helper).and_return(fake_project_helper)
+ allow(change_column_default.project_helper).to receive(:file_lines).and_return(file_lines)
+ allow(change_column_default.helper).to receive(:all_changed_files).and_return([filename])
+ allow(change_column_default.helper).to receive(:changed_lines).with(filename).and_return(file_diff)
+ end
+
+ context 'when column default is changed in a regular migration' do
+ let(:filename) { 'db/migrate/change_column_default_migration.rb' }
+
+ it 'adds comment at the correct line' do
+ matching_lines.each do |line_number|
+ expect(change_column_default).to receive(:markdown).with("\n#{comment}", file: filename, line: line_number)
+ end
+
+ change_column_default.add_comment_for_change_column_default
+ end
+ end
+
+ context 'when column default is changed in a post-deployment migration' do
+ let(:filename) { 'db/post_migrate/change_column_default_migration.rb' }
+
+ it 'adds comment at the correct line' do
+ matching_lines.each do |line_number|
+ expect(change_column_default).to receive(:markdown).with("\n#{comment}", file: filename, line: line_number)
+ end
+
+ change_column_default.add_comment_for_change_column_default
+ end
+ end
+
+ context 'when a regular migration does not change column default' do
+ let(:filename) { 'db/migrate/my_migration.rb' }
+ let(:file_diff) do
+ [
+ "+ undo_cleanup_concurrent_column_rename(:my_table, :old_column, :new_column)",
+ "- cleanup_concurrent_column_rename(:my_table, :new_column, :old_column)"
+ ]
+ end
+
+ let(:file_lines) do
+ [
+ ' def up',
+ ' undo_cleanup_concurrent_column_rename(:my_table, :old_column, :new_column)',
+ ' end'
+ ]
+ end
+
+ it 'does not add comment' do
+ expect(change_column_default).not_to receive(:markdown)
+
+ change_column_default.add_comment_for_change_column_default
+ end
+ end
+
+ context 'when a post-deployment migration does not change column default' do
+ let(:filename) { 'db/post_migrate/my_migration.rb' }
+ let(:file_diff) do
+ [
+ "+ restore_conversion_of_integer_to_bigint(TABLE, COLUMNS)",
+ "- cleanup_conversion_of_integer_to_bigint(TABLE, COLUMNS)"
+ ]
+ end
+
+ let(:file_lines) do
+ [
+ ' def up',
+ ' cleanup_conversion_of_integer_to_bigint(TABLE, COLUMNS)',
+ ' end'
+ ]
+ end
+
+ it 'does not add comment' do
+ expect(change_column_default).not_to receive(:markdown)
+
+ change_column_default.add_comment_for_change_column_default
+ end
+ end
+ end
+end
diff --git a/spec/tooling/danger/clickhouse_spec.rb b/spec/tooling/danger/clickhouse_spec.rb
index ad2f0b4a827..135f8810e61 100644
--- a/spec/tooling/danger/clickhouse_spec.rb
+++ b/spec/tooling/danger/clickhouse_spec.rb
@@ -1,9 +1,7 @@
# frozen_string_literal: true
require 'rspec-parameterized'
-require 'gitlab-dangerfiles'
-require 'danger'
-require 'danger/plugins/internal/helper'
+require 'fast_spec_helper'
require 'gitlab/dangerfiles/spec_helper'
require_relative '../../../tooling/danger/clickhouse'
diff --git a/spec/tooling/danger/config_files_spec.rb b/spec/tooling/danger/config_files_spec.rb
index 42fc08ad901..f9b7dc64e6d 100644
--- a/spec/tooling/danger/config_files_spec.rb
+++ b/spec/tooling/danger/config_files_spec.rb
@@ -1,8 +1,6 @@
# frozen_string_literal: true
-require 'gitlab-dangerfiles'
-require 'danger'
-require 'danger/plugins/internal/helper'
+require 'fast_spec_helper'
require 'gitlab/dangerfiles/spec_helper'
require_relative '../../../tooling/danger/config_files'
diff --git a/spec/tooling/danger/customer_success_spec.rb b/spec/tooling/danger/customer_success_spec.rb
index 798905212f1..40ab7c79418 100644
--- a/spec/tooling/danger/customer_success_spec.rb
+++ b/spec/tooling/danger/customer_success_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'rspec-parameterized'
-require 'gitlab-dangerfiles'
+require 'fast_spec_helper'
require 'gitlab/dangerfiles/spec_helper'
require_relative '../../../tooling/danger/customer_success'
@@ -17,53 +17,53 @@ RSpec.describe Tooling::Danger::CustomerSuccess do
where do
{
'with data category changes to Ops and no Customer Success::Impact Check label' => {
- modified_files: %w(config/metrics/20210216182127_user_secret_detection_jobs.yml app/models/user.rb),
+ modified_files: %w[config/metrics/20210216182127_user_secret_detection_jobs.yml app/models/user.rb],
changed_lines: ['-data_category: cat1', '+data_category: operational'],
customer_labeled: false,
impacted: true,
- impacted_files: %w(config/metrics/20210216182127_user_secret_detection_jobs.yml)
+ impacted_files: %w[config/metrics/20210216182127_user_secret_detection_jobs.yml]
},
'with data category changes and Customer Success::Impact Check label' => {
- modified_files: %w(config/metrics/20210216182127_user_secret_detection_jobs.yml),
+ modified_files: %w[config/metrics/20210216182127_user_secret_detection_jobs.yml],
changed_lines: ['-data_category: cat1', '+data_category: operational'],
customer_labeled: true,
impacted: false,
- impacted_files: %w(config/metrics/20210216182127_user_secret_detection_jobs.yml)
+ impacted_files: %w[config/metrics/20210216182127_user_secret_detection_jobs.yml]
},
'with metric file changes and no data category changes' => {
- modified_files: %w(config/metrics/20210216182127_user_secret_detection_jobs.yml),
+ modified_files: %w[config/metrics/20210216182127_user_secret_detection_jobs.yml],
changed_lines: ['-product_stage: growth'],
customer_labeled: false,
impacted: false,
impacted_files: []
},
'with data category changes from Ops' => {
- modified_files: %w(config/metrics/20210216182127_user_secret_detection_jobs.yml app/models/user.rb),
+ modified_files: %w[config/metrics/20210216182127_user_secret_detection_jobs.yml app/models/user.rb],
changed_lines: ['-data_category: operational', '+data_category: cat2'],
customer_labeled: false,
impacted: true,
- impacted_files: %w(config/metrics/20210216182127_user_secret_detection_jobs.yml)
+ impacted_files: %w[config/metrics/20210216182127_user_secret_detection_jobs.yml]
},
'with data category removed' => {
- modified_files: %w(config/metrics/20210216182127_user_secret_detection_jobs.yml app/models/user.rb),
+ modified_files: %w[config/metrics/20210216182127_user_secret_detection_jobs.yml app/models/user.rb],
changed_lines: ['-data_category: operational'],
customer_labeled: false,
impacted: true,
- impacted_files: %w(config/metrics/20210216182127_user_secret_detection_jobs.yml)
+ impacted_files: %w[config/metrics/20210216182127_user_secret_detection_jobs.yml]
},
'with data category added' => {
- modified_files: %w(config/metrics/20210216182127_user_secret_detection_jobs.yml app/models/user.rb),
+ modified_files: %w[config/metrics/20210216182127_user_secret_detection_jobs.yml app/models/user.rb],
changed_lines: ['+data_category: operational'],
customer_labeled: false,
impacted: true,
- impacted_files: %w(config/metrics/20210216182127_user_secret_detection_jobs.yml)
+ impacted_files: %w[config/metrics/20210216182127_user_secret_detection_jobs.yml]
},
'with data category in uppercase' => {
- modified_files: %w(config/metrics/20210216182127_user_secret_detection_jobs.yml app/models/user.rb),
+ modified_files: %w[config/metrics/20210216182127_user_secret_detection_jobs.yml app/models/user.rb],
changed_lines: ['+data_category: Operational'],
customer_labeled: false,
impacted: true,
- impacted_files: %w(config/metrics/20210216182127_user_secret_detection_jobs.yml)
+ impacted_files: %w[config/metrics/20210216182127_user_secret_detection_jobs.yml]
}
}
end
diff --git a/spec/tooling/danger/database_dictionary_spec.rb b/spec/tooling/danger/database_dictionary_spec.rb
index 1a771a6cec0..943179cda19 100644
--- a/spec/tooling/danger/database_dictionary_spec.rb
+++ b/spec/tooling/danger/database_dictionary_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'gitlab-dangerfiles'
+require 'fast_spec_helper'
require 'gitlab/dangerfiles/spec_helper'
require_relative '../../../tooling/danger/database_dictionary'
diff --git a/spec/tooling/danger/database_spec.rb b/spec/tooling/danger/database_spec.rb
index a342014cf6b..bfc92e0a744 100644
--- a/spec/tooling/danger/database_spec.rb
+++ b/spec/tooling/danger/database_spec.rb
@@ -1,9 +1,7 @@
# frozen_string_literal: true
+require 'fast_spec_helper'
require 'rspec-parameterized'
-require 'gitlab-dangerfiles'
-require 'danger'
-require 'danger/plugins/internal/helper'
require 'gitlab/dangerfiles/spec_helper'
require_relative '../../../tooling/danger/database'
diff --git a/spec/tooling/danger/datateam_spec.rb b/spec/tooling/danger/datateam_spec.rb
index de8a93baa27..9d8aaf08520 100644
--- a/spec/tooling/danger/datateam_spec.rb
+++ b/spec/tooling/danger/datateam_spec.rb
@@ -1,9 +1,8 @@
# frozen_string_literal: true
+require 'fast_spec_helper'
require 'rspec-parameterized'
-require 'gitlab-dangerfiles'
require 'gitlab/dangerfiles/spec_helper'
-require 'pry'
require_relative '../../../tooling/danger/datateam'
RSpec.describe Tooling::Danger::Datateam do
@@ -17,89 +16,96 @@ RSpec.describe Tooling::Danger::Datateam do
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'],
+ 'with structure.sql subtraction 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)
+ 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)'],
+ 'with structure.sql subtraction 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)
+ impacted_files: %w[db/structure.sql]
+ },
+ 'with structure.sql addition 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: false,
+ impacted_files: %w[db/structure.sql]
},
'with user model changes' => {
- modified_files: %w(app/models/users.rb),
+ 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),
+ 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)
+ 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),
+ 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)
+ 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),
+ 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),
+ modified_files: %w[config/metrics/20210216182127_user_secret_detection_jobs.yml],
changed_lines: ['-product_stage: growth'],
mr_labels: ['type::maintenance'],
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),
+ modified_files: %w[config/metrics/20210216182127_user_secret_detection_jobs.yml app/models/user.rb],
changed_lines: ['+-gmau'],
mr_labels: ['type::maintenance'],
impacted: true,
- impacted_files: %w(config/metrics/20210216182127_user_secret_detection_jobs.yml)
+ 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),
+ modified_files: %w[config/metrics/20210216182127_user_secret_detection_jobs.yml app/models/user.rb],
changed_lines: ['+-gmau'],
mr_labels: ['type::maintenance', 'Data Warehouse::Impact Check'],
impacted: false,
- impacted_files: %w(config/metrics/20210216182127_user_secret_detection_jobs.yml)
+ 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),
+ modified_files: %w[config/metrics/20210216182127_user_secret_detection_jobs.yml app/models/user.rb],
changed_lines: ['+-gmau'],
mr_labels: ['type::maintenance', 'Data Warehouse::Impacted'],
impacted: false,
- impacted_files: %w(config/metrics/20210216182127_user_secret_detection_jobs.yml)
+ impacted_files: %w[config/metrics/20210216182127_user_secret_detection_jobs.yml]
},
'with metric status removed' => {
- modified_files: %w(config/metrics/20210216182127_user_secret_detection_jobs.yml app/models/user.rb),
+ modified_files: %w[config/metrics/20210216182127_user_secret_detection_jobs.yml app/models/user.rb],
changed_lines: ['+status: removed'],
mr_labels: ['type::maintenance'],
impacted: true,
- impacted_files: %w(config/metrics/20210216182127_user_secret_detection_jobs.yml)
+ impacted_files: %w[config/metrics/20210216182127_user_secret_detection_jobs.yml]
},
'with metric status active' => {
- modified_files: %w(config/metrics/20210216182127_user_secret_detection_jobs.yml app/models/user.rb),
+ modified_files: %w[config/metrics/20210216182127_user_secret_detection_jobs.yml app/models/user.rb],
changed_lines: ['+status: active'],
mr_labels: ['type::maintenance'],
impacted: false,
- impacted_files: %w(config/metrics/20210216182127_user_secret_detection_jobs.yml)
+ impacted_files: %w[config/metrics/20210216182127_user_secret_detection_jobs.yml]
}
}
end
diff --git a/spec/tooling/danger/experiments_spec.rb b/spec/tooling/danger/experiments_spec.rb
index 85f8060a3ec..7c4921670a3 100644
--- a/spec/tooling/danger/experiments_spec.rb
+++ b/spec/tooling/danger/experiments_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'gitlab-dangerfiles'
+require 'fast_spec_helper'
require 'gitlab/dangerfiles/spec_helper'
require_relative '../../../tooling/danger/experiments'
diff --git a/spec/tooling/danger/feature_flag_spec.rb b/spec/tooling/danger/feature_flag_spec.rb
index 4575d8ca981..9298028feb3 100644
--- a/spec/tooling/danger/feature_flag_spec.rb
+++ b/spec/tooling/danger/feature_flag_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'gitlab-dangerfiles'
+require 'fast_spec_helper'
require 'gitlab/dangerfiles/spec_helper'
require_relative '../../../tooling/danger/feature_flag'
@@ -134,7 +134,7 @@ RSpec.describe Tooling::Danger::FeatureFlag do
context 'when group is not nil' do
it 'is true only if MR has the same group label' do
expect(found.group_match_mr_label?(group)).to eq true
- expect(found.group_match_mr_label?('group::authentication and authorization')).to eq false
+ expect(found.group_match_mr_label?('group::authentication')).to eq false
end
end
end
diff --git a/spec/tooling/danger/gitlab_schema_validation_suggestion_spec.rb b/spec/tooling/danger/gitlab_schema_validation_suggestion_spec.rb
new file mode 100644
index 00000000000..c83e0319423
--- /dev/null
+++ b/spec/tooling/danger/gitlab_schema_validation_suggestion_spec.rb
@@ -0,0 +1,108 @@
+# frozen_string_literal: true
+
+require 'fast_spec_helper'
+require 'gitlab/dangerfiles/spec_helper'
+
+require_relative '../../../tooling/danger/gitlab_schema_validation_suggestion'
+require_relative '../../../tooling/danger/project_helper'
+
+RSpec.describe Tooling::Danger::GitlabSchemaValidationSuggestion, feature_category: :cell do
+ include_context "with dangerfile"
+
+ let(:fake_danger) { DangerSpecHelper.fake_danger.include(described_class) }
+ let(:fake_project_helper) { instance_double(Tooling::Danger::ProjectHelper) }
+ let(:filename) { 'db/docs/application_settings.yml' }
+ let(:file_lines) do
+ file_diff.map { |line| line.delete_prefix('+') }
+ end
+
+ let(:file_diff) do
+ [
+ "+---",
+ "+table_name: application_settings",
+ "+classes:",
+ "+- ApplicationSetting",
+ "+feature_categories:",
+ "+- continuous_integration",
+ "+description: GitLab application settings",
+ "+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/commit/8589b4e137f50293952923bb07e2814257d7784d",
+ "+milestone: '7.7'",
+ "+gitlab_schema: #{schema}"
+ ]
+ end
+
+ subject(:gitlab_schema_validation) { fake_danger.new(helper: fake_helper) }
+
+ before do
+ allow(gitlab_schema_validation).to receive(:project_helper).and_return(fake_project_helper)
+ allow(gitlab_schema_validation.project_helper).to receive(:file_lines).and_return(file_lines)
+ allow(gitlab_schema_validation.helper).to receive(:changed_lines).with(filename).and_return(file_diff)
+ allow(gitlab_schema_validation.helper).to receive(:all_changed_files).and_return([filename])
+ end
+
+ shared_examples_for 'does not add a comment' do
+ it do
+ expect(gitlab_schema_validation).not_to receive(:markdown)
+
+ gitlab_schema_validation.add_suggestions_on_using_clusterwide_schema
+ end
+ end
+
+ context 'for discouraging the use of gitlab_main_clusterwide schema' do
+ let(:schema) { 'gitlab_main_clusterwide' }
+
+ context 'when the file path matches' do
+ it 'adds the comment' do
+ expected_comment = "\n#{described_class::SUGGESTION.chomp}"
+
+ expect(gitlab_schema_validation).to receive(:markdown).with(expected_comment, file: filename, line: 10)
+
+ gitlab_schema_validation.add_suggestions_on_using_clusterwide_schema
+ end
+ end
+
+ context 'when the file path does not match' do
+ let(:filename) { 'some_path/application_settings.yml' }
+
+ it_behaves_like 'does not add a comment'
+ end
+
+ context 'for EE' do
+ let(:filename) { 'ee/db/docs/application_settings.yml' }
+
+ it_behaves_like 'does not add a comment'
+ end
+
+ context 'for a deleted table' do
+ let(:filename) { 'db/docs/deleted_tables/application_settings.yml' }
+
+ it_behaves_like 'does not add a comment'
+ end
+ end
+
+ context 'on removing the gitlab_main_clusterwide schema' do
+ let(:file_diff) do
+ [
+ "+---",
+ "+table_name: application_settings",
+ "+classes:",
+ "+- ApplicationSetting",
+ "+feature_categories:",
+ "+- continuous_integration",
+ "+description: GitLab application settings",
+ "+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/commit/8589b4e137f50293952923bb07e2814257d7784d",
+ "+milestone: '7.7'",
+ "-gitlab_schema: gitlab_main_clusterwide",
+ "+gitlab_schema: gitlab_main_cell"
+ ]
+ end
+
+ it_behaves_like 'does not add a comment'
+ end
+
+ context 'when a different schema is added' do
+ let(:schema) { 'gitlab_main' }
+
+ it_behaves_like 'does not add a comment'
+ end
+end
diff --git a/spec/tooling/danger/ignored_model_columns_spec.rb b/spec/tooling/danger/ignored_model_columns_spec.rb
index 737b6cce077..3d19f80a4ed 100644
--- a/spec/tooling/danger/ignored_model_columns_spec.rb
+++ b/spec/tooling/danger/ignored_model_columns_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'danger'
+require 'fast_spec_helper'
require 'gitlab/dangerfiles/spec_helper'
require_relative '../../../tooling/danger/ignored_model_columns'
diff --git a/spec/tooling/danger/model_validations_spec.rb b/spec/tooling/danger/model_validations_spec.rb
index 18ff4b83b6e..2dc2bc3e186 100644
--- a/spec/tooling/danger/model_validations_spec.rb
+++ b/spec/tooling/danger/model_validations_spec.rb
@@ -1,8 +1,6 @@
# frozen_string_literal: true
-require 'gitlab-dangerfiles'
-require 'danger'
-require 'danger/plugins/internal/helper'
+require 'fast_spec_helper'
require 'gitlab/dangerfiles/spec_helper'
require_relative '../../../tooling/danger/model_validations'
diff --git a/spec/tooling/danger/multiversion_spec.rb b/spec/tooling/danger/multiversion_spec.rb
index 90edad61d47..649a13013c4 100644
--- a/spec/tooling/danger/multiversion_spec.rb
+++ b/spec/tooling/danger/multiversion_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'rspec-parameterized'
-require 'gitlab-dangerfiles'
+require 'fast_spec_helper'
require 'gitlab/dangerfiles/spec_helper'
require_relative '../../../tooling/danger/multiversion'
diff --git a/spec/tooling/danger/outdated_todo_spec.rb b/spec/tooling/danger/outdated_todo_spec.rb
new file mode 100644
index 00000000000..3a3909c69ac
--- /dev/null
+++ b/spec/tooling/danger/outdated_todo_spec.rb
@@ -0,0 +1,83 @@
+# frozen_string_literal: true
+
+require 'fast_spec_helper'
+require 'gitlab/dangerfiles/spec_helper'
+
+require_relative '../../../tooling/danger/outdated_todo'
+
+RSpec.describe Tooling::Danger::OutdatedTodo, feature_category: :tooling do
+ let(:fake_danger) { double }
+ let(:filenames) { ['app/controllers/application_controller.rb'] }
+
+ let(:todos) do
+ [
+ File.join('spec', 'fixtures', 'tooling', 'danger', 'rubocop_todo', '**', '*.yml')
+ ]
+ end
+
+ subject(:plugin) { described_class.new(filenames, context: fake_danger, todos: todos) }
+
+ context 'when the filenames are mentioned in single todo' do
+ let(:filenames) { ['app/controllers/acme_challenges_controller.rb'] }
+
+ it 'warns about mentions' do
+ expect(fake_danger)
+ .to receive(:warn)
+ .with <<~MESSAGE
+ `app/controllers/acme_challenges_controller.rb` was removed but is mentioned in:
+ - `spec/fixtures/tooling/danger/rubocop_todo/cop1.yml:5`
+ MESSAGE
+
+ plugin.check
+ end
+ end
+
+ context 'when the filenames are mentioned in multiple todos' do
+ let(:filenames) do
+ [
+ 'app/controllers/application_controller.rb',
+ 'app/controllers/acme_challenges_controller.rb'
+ ]
+ end
+
+ it 'warns about mentions' do
+ expect(fake_danger)
+ .to receive(:warn)
+ .with(<<~FIRSTMESSAGE)
+ `app/controllers/application_controller.rb` was removed but is mentioned in:
+ - `spec/fixtures/tooling/danger/rubocop_todo/cop1.yml:4`
+ - `spec/fixtures/tooling/danger/rubocop_todo/cop2.yml:4`
+ FIRSTMESSAGE
+
+ expect(fake_danger)
+ .to receive(:warn)
+ .with(<<~SECONDMESSAGE)
+ `app/controllers/acme_challenges_controller.rb` was removed but is mentioned in:
+ - `spec/fixtures/tooling/danger/rubocop_todo/cop1.yml:5`
+ SECONDMESSAGE
+
+ plugin.check
+ end
+ end
+
+ context 'when the filenames are not mentioned in todos' do
+ let(:filenames) { ['any/inexisting/file.rb'] }
+
+ it 'does not warn' do
+ expect(fake_danger).not_to receive(:warn)
+
+ plugin.check
+ end
+ end
+
+ context 'when there is no todos' do
+ let(:filenames) { ['app/controllers/acme_challenges_controller.rb'] }
+ let(:todos) { [] }
+
+ it 'does not warn' do
+ expect(fake_danger).not_to receive(:warn)
+
+ plugin.check
+ end
+ end
+end
diff --git a/spec/tooling/danger/project_helper_spec.rb b/spec/tooling/danger/project_helper_spec.rb
index 28b8b2278d0..2da90ddbd67 100644
--- a/spec/tooling/danger/project_helper_spec.rb
+++ b/spec/tooling/danger/project_helper_spec.rb
@@ -1,11 +1,10 @@
# frozen_string_literal: true
+require 'fast_spec_helper'
require 'rspec-parameterized'
-require 'gitlab-dangerfiles'
require 'danger'
require 'danger/plugins/internal/helper'
require 'gitlab/dangerfiles/spec_helper'
-require 'gitlab/rspec/all'
require_relative '../../../danger/plugins/project_helper'
@@ -244,6 +243,7 @@ RSpec.describe Tooling::Danger::ProjectHelper do
[:analytics_instrumentation] | '+data-track-action' | ['components/welcome.vue']
[:analytics_instrumentation] | '+ data: { track_label:' | ['admin/groups/_form.html.haml']
[:analytics_instrumentation] | '+ Gitlab::Tracking.event' | ['dashboard/todos_controller.rb', 'admin/groups/_form.html.haml']
+ [:analytics_instrumentation] | '+ Gitlab::Tracking.event("c", "a")' | ['dashboard/todos_controller.rb', 'admin/groups/_form.html.haml']
[:database, :backend, :analytics_instrumentation] | '+ count(User.active)' | ['usage_data.rb', 'lib/gitlab/usage_data.rb', 'ee/lib/ee/gitlab/usage_data.rb']
[:database, :backend, :analytics_instrumentation] | '+ estimate_batch_distinct_count(User.active)' | ['usage_data.rb']
[:backend, :analytics_instrumentation] | '+ alt_usage_data(User.active)' | ['lib/gitlab/usage_data.rb']
diff --git a/spec/tooling/danger/required_stops_spec.rb b/spec/tooling/danger/required_stops_spec.rb
index 7a90f19ac09..1b811166d13 100644
--- a/spec/tooling/danger/required_stops_spec.rb
+++ b/spec/tooling/danger/required_stops_spec.rb
@@ -1,8 +1,6 @@
# frozen_string_literal: true
-require 'gitlab-dangerfiles'
-require 'danger'
-require 'danger/plugins/internal/helper'
+require 'fast_spec_helper'
require 'gitlab/dangerfiles/spec_helper'
require_relative '../../../tooling/danger/required_stops'
diff --git a/spec/tooling/danger/rubocop_inline_disable_suggestion_spec.rb b/spec/tooling/danger/rubocop_inline_disable_suggestion_spec.rb
index 94dd5192d74..6b9ff667564 100644
--- a/spec/tooling/danger/rubocop_inline_disable_suggestion_spec.rb
+++ b/spec/tooling/danger/rubocop_inline_disable_suggestion_spec.rb
@@ -1,5 +1,6 @@
# frozen_string_literal: true
+require 'fast_spec_helper'
require 'gitlab/dangerfiles/spec_helper'
require_relative '../../../tooling/danger/rubocop_inline_disable_suggestion'
@@ -14,10 +15,15 @@ RSpec.describe Tooling::Danger::RubocopInlineDisableSuggestion, feature_category
let(:template) do
<<~SUGGESTION_MARKDOWN.chomp
+ ```suggestion
+ %<suggested_line>s
+ ```
Consider removing this inline disabling and adhering to the rubocop rule.
- If that isn't possible, please provide context as a reply for reviewers.
- See [rubocop best practices](https://docs.gitlab.com/ee/development/rubocop_development_guide.html).
+
+ If that isn't possible, please provide the reason as a code comment in the
+ same line where the rule is disabled separated by ` -- `.
+ See [rubocop best practices](https://docs.gitlab.com/ee/development/rubocop_development_guide.html#disabling-rules-inline).
----
@@ -73,6 +79,23 @@ RSpec.describe Tooling::Danger::RubocopInlineDisableSuggestion, feature_category
show_out_of_pipeline_minutes_notification?(project, namespace)
end
+
+ def show_my_new_dot?(project, namespace)
+ return false unless ::Gitlab.com? # rubocop: todo Gitlab/AvoidGitlabInstanceChecks -- Reason for disabling
+ thatsfine = "".dup # rubocop:disable Lint/UselessAssignment,Performance/UnfreezeString -- That's OK
+ me = "".dup # rubocop:disable Lint/UselessAssignment,Performance/UnfreezeString
+ test = "".dup # rubocop:disable Lint/UselessAssignment, Performance/UnfreezeString
+ return false if notification_dot_acknowledged?
+
+ show_out_of_pipeline_minutes_notification?(project, namespace)
+ end
+
+ def show_my_bad_dot?(project, namespace)
+ return false unless ::Gitlab.com? # rubocop: todo Gitlab/AvoidGitlabInstanceChecks --
+ return false if notification_dot_acknowledged?
+
+ show_out_of_pipeline_minutes_notification?(project, namespace)
+ end
RUBY
end
@@ -86,6 +109,10 @@ RSpec.describe Tooling::Danger::RubocopInlineDisableSuggestion, feature_category
+ return false unless ::Gitlab.com? # rubocop: disable Gitlab/AvoidGitlabInstanceChecks
+ return false unless ::Gitlab.com? # rubocop:todo Gitlab/AvoidGitlabInstanceChecks
+ return false unless ::Gitlab.com? # rubocop: todo Gitlab/AvoidGitlabInstanceChecks
+ + return false unless ::Gitlab.com? # rubocop: todo Gitlab/AvoidGitlabInstanceChecks -- Reason for disabling
+ + me = "".dup # rubocop:disable Lint/UselessAssignment,Performance/UnfreezeString
+ + test = "".dup # rubocop:disable Lint/UselessAssignment, Performance/UnfreezeString
+ + return false unless ::Gitlab.com? # rubocop: todo Gitlab/AvoidGitlabInstanceChecks --
DIFF
end
@@ -102,8 +129,12 @@ RSpec.describe Tooling::Danger::RubocopInlineDisableSuggestion, feature_category
end
it 'adds comments at the correct lines', :aggregate_failures do
- [3, 7, 13, 20, 27, 34, 41].each do |line_number|
- expect(rubocop).to receive(:markdown).with(template, file: filename, line: line_number)
+ [3, 7, 13, 20, 27, 34, 41, 50, 51, 58].each do |line_number|
+ existing_line = file_lines[line_number - 1].sub(/ --\s*$/, '')
+ suggested_line = "#{existing_line} -- TODO: Reason why the rule must be disabled"
+ comment = format(template, suggested_line: suggested_line)
+
+ expect(rubocop).to receive(:markdown).with(comment, file: filename, line: line_number)
end
rubocop.add_suggestions_for(filename)
diff --git a/spec/tooling/danger/saas_feature_spec.rb b/spec/tooling/danger/saas_feature_spec.rb
index 7ce9116ea5f..019dbf6944f 100644
--- a/spec/tooling/danger/saas_feature_spec.rb
+++ b/spec/tooling/danger/saas_feature_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'gitlab-dangerfiles'
+require 'fast_spec_helper'
require 'gitlab/dangerfiles/spec_helper'
require_relative '../../../tooling/danger/saas_feature'
diff --git a/spec/tooling/danger/sidekiq_args_spec.rb b/spec/tooling/danger/sidekiq_args_spec.rb
index 29bf32a9a02..c44486a83b5 100644
--- a/spec/tooling/danger/sidekiq_args_spec.rb
+++ b/spec/tooling/danger/sidekiq_args_spec.rb
@@ -1,9 +1,7 @@
# frozen_string_literal: true
require 'rspec-parameterized'
-require 'gitlab-dangerfiles'
-require 'danger'
-require 'danger/plugins/internal/helper'
+require 'fast_spec_helper'
require 'gitlab/dangerfiles/spec_helper'
require_relative '../../../tooling/danger/sidekiq_args'
diff --git a/spec/tooling/danger/sidekiq_queues_spec.rb b/spec/tooling/danger/sidekiq_queues_spec.rb
index 9bffc7ee93d..143ea9732cd 100644
--- a/spec/tooling/danger/sidekiq_queues_spec.rb
+++ b/spec/tooling/danger/sidekiq_queues_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
+require 'fast_spec_helper'
require 'rspec-parameterized'
-require 'gitlab-dangerfiles'
require 'gitlab/dangerfiles/spec_helper'
require_relative '../../../tooling/danger/sidekiq_queues'
@@ -17,12 +17,12 @@ RSpec.describe Tooling::Danger::SidekiqQueues do
using RSpec::Parameterized::TableSyntax
where(:modified_files, :changed_queue_files) do
- %w(app/workers/all_queues.yml ee/app/workers/all_queues.yml foo) | %w(app/workers/all_queues.yml ee/app/workers/all_queues.yml)
- %w(app/workers/all_queues.yml ee/app/workers/all_queues.yml) | %w(app/workers/all_queues.yml ee/app/workers/all_queues.yml)
- %w(app/workers/all_queues.yml foo) | %w(app/workers/all_queues.yml)
- %w(ee/app/workers/all_queues.yml foo) | %w(ee/app/workers/all_queues.yml)
- %w(foo) | %w()
- %w() | %w()
+ %w[app/workers/all_queues.yml ee/app/workers/all_queues.yml foo] | %w[app/workers/all_queues.yml ee/app/workers/all_queues.yml]
+ %w[app/workers/all_queues.yml ee/app/workers/all_queues.yml] | %w[app/workers/all_queues.yml ee/app/workers/all_queues.yml]
+ %w[app/workers/all_queues.yml foo] | %w[app/workers/all_queues.yml]
+ %w[ee/app/workers/all_queues.yml foo] | %w[ee/app/workers/all_queues.yml]
+ %w[foo] | %w[]
+ %w[] | %w[]
end
with_them do
diff --git a/spec/tooling/danger/specs/feature_category_suggestion_spec.rb b/spec/tooling/danger/specs/feature_category_suggestion_spec.rb
index 87eb20e5e50..ea8a00afac1 100644
--- a/spec/tooling/danger/specs/feature_category_suggestion_spec.rb
+++ b/spec/tooling/danger/specs/feature_category_suggestion_spec.rb
@@ -1,5 +1,6 @@
# frozen_string_literal: true
+require 'fast_spec_helper'
require 'gitlab/dangerfiles/spec_helper'
require_relative '../../../../tooling/danger/specs'
diff --git a/spec/tooling/danger/specs/match_with_array_suggestion_spec.rb b/spec/tooling/danger/specs/match_with_array_suggestion_spec.rb
index b065772a09b..92c4a134a34 100644
--- a/spec/tooling/danger/specs/match_with_array_suggestion_spec.rb
+++ b/spec/tooling/danger/specs/match_with_array_suggestion_spec.rb
@@ -1,5 +1,6 @@
# frozen_string_literal: true
+require 'fast_spec_helper'
require 'gitlab/dangerfiles/spec_helper'
require_relative '../../../../tooling/danger/specs'
diff --git a/spec/tooling/danger/specs/project_factory_suggestion_spec.rb b/spec/tooling/danger/specs/project_factory_suggestion_spec.rb
index b765d5073af..078f415cc44 100644
--- a/spec/tooling/danger/specs/project_factory_suggestion_spec.rb
+++ b/spec/tooling/danger/specs/project_factory_suggestion_spec.rb
@@ -1,5 +1,6 @@
# frozen_string_literal: true
+require 'fast_spec_helper'
require 'gitlab/dangerfiles/spec_helper'
require_relative '../../../../tooling/danger/specs'
diff --git a/spec/tooling/danger/specs_spec.rb b/spec/tooling/danger/specs_spec.rb
index b4953858ef7..f601e11a7a5 100644
--- a/spec/tooling/danger/specs_spec.rb
+++ b/spec/tooling/danger/specs_spec.rb
@@ -1,5 +1,6 @@
# frozen_string_literal: true
+require 'fast_spec_helper'
require 'gitlab/dangerfiles/spec_helper'
require_relative '../../../tooling/danger/specs'
diff --git a/spec/tooling/danger/stable_branch_spec.rb b/spec/tooling/danger/stable_branch_spec.rb
index 69e68f983fd..a33788f54f2 100644
--- a/spec/tooling/danger/stable_branch_spec.rb
+++ b/spec/tooling/danger/stable_branch_spec.rb
@@ -1,8 +1,8 @@
# frozen_string_literal: true
-require 'gitlab-dangerfiles'
-require 'gitlab/dangerfiles/spec_helper'
require 'rspec-parameterized'
+require 'fast_spec_helper'
+require 'gitlab/dangerfiles/spec_helper'
require 'httparty'
require_relative '../../../tooling/danger/stable_branch'
diff --git a/spec/tooling/fixtures/change_column_default_migration.txt b/spec/tooling/fixtures/change_column_default_migration.txt
new file mode 100644
index 00000000000..a74c31464ee
--- /dev/null
+++ b/spec/tooling/fixtures/change_column_default_migration.txt
@@ -0,0 +1,13 @@
++# frozen_string_literal: true
++
++class TestMigration < Gitlab::Database::Migration[2.1]
++ enable_lock_retries!
++
++ def change
++ change_column_default('ci_builds', 'partition_id', from: 100, to: 101)
++
++ change_column_default('ci_builds', 'partition_id', from: 100, to: 101)
++
++ remove_column_default('ci_builds', 'partition_id')
++ end
++end
diff --git a/spec/tooling/lib/tooling/find_changes_spec.rb b/spec/tooling/lib/tooling/find_changes_spec.rb
index fef29ad3f2c..85e3eadac6f 100644
--- a/spec/tooling/lib/tooling/find_changes_spec.rb
+++ b/spec/tooling/lib/tooling/find_changes_spec.rb
@@ -15,7 +15,8 @@ RSpec.describe Tooling::FindChanges, feature_category: :tooling do
changed_files_pathname: changed_files_pathname,
predictive_tests_pathname: predictive_tests_pathname,
frontend_fixtures_mapping_pathname: frontend_fixtures_mapping_pathname,
- from: from)
+ from: from,
+ file_filter: file_filter)
end
let(:changed_files_pathname) { changed_files_file.path }
@@ -23,6 +24,7 @@ RSpec.describe Tooling::FindChanges, feature_category: :tooling do
let(:frontend_fixtures_mapping_pathname) { frontend_fixtures_mapping_file.path }
let(:from) { :api }
let(:gitlab_client) { double('GitLab') } # rubocop:disable RSpec/VerifiedDoubles
+ let(:file_filter) { ->(_) { true } }
around do |example|
self.changed_files_file = Tempfile.new('changed_files_file')
@@ -89,6 +91,37 @@ RSpec.describe Tooling::FindChanges, feature_category: :tooling do
subject
end
+
+ context 'when used with file_filter' do
+ let(:file_filter) { ->(file) { file['new_path'] =~ %r{doc/.*} } }
+
+ let(:mr_changes_array) do
+ [
+ {
+ "new_path" => "scripts/test.js",
+ "old_path" => "scripts/test.js"
+ },
+ {
+ "new_path" => "doc/index.md",
+ "old_path" => "doc/index.md"
+ }
+ ]
+ end
+
+ before do
+ # rubocop:disable RSpec/VerifiedDoubles -- The class from the GitLab gem isn't public, so we cannot use verified doubles for it.
+ allow(gitlab_client).to receive(:merge_request_changes)
+ .with('dummy-project', '1234')
+ .and_return(double(changes: mr_changes_array))
+ # rubocop:enable RSpec/VerifiedDoubles
+ end
+
+ it 'only writes matching files to output' do
+ subject
+
+ expect(File.read(changed_files_file)).to eq('doc/index.md')
+ end
+ end
end
context 'when fetching changes from changed files' do
diff --git a/spec/tooling/lib/tooling/test_map_generator_spec.rb b/spec/tooling/lib/tooling/test_map_generator_spec.rb
index 1b369923d8d..eaaf525fc49 100644
--- a/spec/tooling/lib/tooling/test_map_generator_spec.rb
+++ b/spec/tooling/lib/tooling/test_map_generator_spec.rb
@@ -88,7 +88,7 @@ RSpec.describe Tooling::TestMapGenerator do
end
it 'displays a warning when report has no examples' do
- expect { subject.parse('yaml3.yml') }.to output(%|No examples in yaml3.yml! Metadata: {:type=>"Crystalball::ExecutionMap", :commit=>"74056e8d9cf3773f43faa1cf5416f8779c8284c9", :timestamp=>1602671965, :version=>nil}\n|).to_stdout
+ expect { subject.parse('yaml3.yml') }.to output(%(No examples in yaml3.yml! Metadata: {:type=>"Crystalball::ExecutionMap", :commit=>"74056e8d9cf3773f43faa1cf5416f8779c8284c9", :timestamp=>1602671965, :version=>nil}\n)).to_stdout
end
end
diff --git a/spec/tooling/quality/test_level_spec.rb b/spec/tooling/quality/test_level_spec.rb
index 6ccd2e46f7b..d7d04015b48 100644
--- a/spec/tooling/quality/test_level_spec.rb
+++ b/spec/tooling/quality/test_level_spec.rb
@@ -46,7 +46,7 @@ RSpec.describe Quality::TestLevel, feature_category: :tooling do
context 'when level is unit' do
it 'returns a pattern' do
expect(subject.pattern(:unit))
- .to eq("spec/{bin,channels,components,config,contracts,db,dependencies,elastic,elastic_integration,experiments,factories,finders,frontend,graphql,haml_lint,helpers,initializers,lib,metrics_server,models,policies,presenters,rack_servers,replicators,routing,rubocop,scripts,serializers,services,sidekiq,sidekiq_cluster,spam,support_specs,tasks,uploaders,validators,views,workers,tooling}{,/**/}*_spec.rb")
+ .to eq("spec/{bin,channels,click_house,components,config,contracts,db,dependencies,elastic,elastic_integration,experiments,factories,finders,frontend,graphql,haml_lint,helpers,initializers,lib,metrics_server,models,policies,presenters,rack_servers,replicators,routing,rubocop,scripts,serializers,services,sidekiq,sidekiq_cluster,spam,support_specs,tasks,uploaders,validators,views,workers,tooling}{,/**/}*_spec.rb")
end
end
@@ -121,7 +121,7 @@ RSpec.describe Quality::TestLevel, feature_category: :tooling do
context 'when level is unit' do
it 'returns a regexp' do
expect(subject.regexp(:unit))
- .to eq(%r{spec/(bin|channels|components|config|contracts|db|dependencies|elastic|elastic_integration|experiments|factories|finders|frontend|graphql|haml_lint|helpers|initializers|lib|metrics_server|models|policies|presenters|rack_servers|replicators|routing|rubocop|scripts|serializers|services|sidekiq|sidekiq_cluster|spam|support_specs|tasks|uploaders|validators|views|workers|tooling)/})
+ .to eq(%r{spec/(bin|channels|click_house|components|config|contracts|db|dependencies|elastic|elastic_integration|experiments|factories|finders|frontend|graphql|haml_lint|helpers|initializers|lib|metrics_server|models|policies|presenters|rack_servers|replicators|routing|rubocop|scripts|serializers|services|sidekiq|sidekiq_cluster|spam|support_specs|tasks|uploaders|validators|views|workers|tooling)/})
end
end
diff --git a/spec/uploaders/attachment_uploader_spec.rb b/spec/uploaders/attachment_uploader_spec.rb
index a035402e207..e4e96aa15b7 100644
--- a/spec/uploaders/attachment_uploader_spec.rb
+++ b/spec/uploaders/attachment_uploader_spec.rb
@@ -10,9 +10,9 @@ RSpec.describe AttachmentUploader do
subject { uploader }
it_behaves_like 'builds correct paths',
- store_dir: %r[uploads/-/system/note/attachment/],
- upload_path: %r[uploads/-/system/note/attachment/],
- absolute_path: %r[#{CarrierWave.root}/uploads/-/system/note/attachment/]
+ store_dir: %r{uploads/-/system/note/attachment/},
+ upload_path: %r{uploads/-/system/note/attachment/},
+ absolute_path: %r{#{CarrierWave.root}/uploads/-/system/note/attachment/}
context "object_store is REMOTE" do
before do
@@ -22,8 +22,8 @@ RSpec.describe AttachmentUploader do
include_context 'with storage', described_class::Store::REMOTE
it_behaves_like 'builds correct paths',
- store_dir: %r[note/attachment/],
- upload_path: %r[note/attachment/]
+ store_dir: %r{note/attachment/},
+ upload_path: %r{note/attachment/}
end
describe "#migrate!" do
diff --git a/spec/uploaders/avatar_uploader_spec.rb b/spec/uploaders/avatar_uploader_spec.rb
index bba7eb78f99..333f23d7947 100644
--- a/spec/uploaders/avatar_uploader_spec.rb
+++ b/spec/uploaders/avatar_uploader_spec.rb
@@ -10,9 +10,9 @@ RSpec.describe AvatarUploader do
subject { uploader }
it_behaves_like 'builds correct paths',
- store_dir: %r[uploads/-/system/user/avatar/],
- upload_path: %r[uploads/-/system/user/avatar/],
- absolute_path: %r[#{CarrierWave.root}/uploads/-/system/user/avatar/]
+ store_dir: %r{uploads/-/system/user/avatar/},
+ upload_path: %r{uploads/-/system/user/avatar/},
+ absolute_path: %r{#{CarrierWave.root}/uploads/-/system/user/avatar/}
context "object_store is REMOTE" do
before do
@@ -22,8 +22,8 @@ RSpec.describe AvatarUploader do
include_context 'with storage', described_class::Store::REMOTE
it_behaves_like 'builds correct paths',
- store_dir: %r[user/avatar/],
- upload_path: %r[user/avatar/]
+ store_dir: %r{user/avatar/},
+ upload_path: %r{user/avatar/}
end
context "with a file" do
diff --git a/spec/uploaders/ci/pipeline_artifact_uploader_spec.rb b/spec/uploaders/ci/pipeline_artifact_uploader_spec.rb
index 3935f081372..ace7bcbce48 100644
--- a/spec/uploaders/ci/pipeline_artifact_uploader_spec.rb
+++ b/spec/uploaders/ci/pipeline_artifact_uploader_spec.rb
@@ -10,8 +10,8 @@ RSpec.describe Ci::PipelineArtifactUploader do
it_behaves_like "builds correct paths",
store_dir: %r[\h{2}/\h{2}/\h{64}/pipelines/\d+/artifacts/\d+],
- cache_dir: %r[artifacts/tmp/cache],
- work_dir: %r[artifacts/tmp/work]
+ cache_dir: %r{artifacts/tmp/cache},
+ work_dir: %r{artifacts/tmp/work}
context 'when object store is REMOTE' do
before do
diff --git a/spec/uploaders/dependency_proxy/file_uploader_spec.rb b/spec/uploaders/dependency_proxy/file_uploader_spec.rb
index 3cb2d1ea0f0..faaa5541f0b 100644
--- a/spec/uploaders/dependency_proxy/file_uploader_spec.rb
+++ b/spec/uploaders/dependency_proxy/file_uploader_spec.rb
@@ -12,8 +12,8 @@ RSpec.describe DependencyProxy::FileUploader do
it_behaves_like "builds correct paths",
store_dir: %r[\h{2}/\h{2}],
- cache_dir: %r[/dependency_proxy/tmp/cache],
- work_dir: %r[/dependency_proxy/tmp/work]
+ cache_dir: %r{/dependency_proxy/tmp/cache},
+ work_dir: %r{/dependency_proxy/tmp/work}
context 'object store is remote' do
before do
diff --git a/spec/uploaders/design_management/design_v432x230_uploader_spec.rb b/spec/uploaders/design_management/design_v432x230_uploader_spec.rb
index 3991058b32d..edfab24331c 100644
--- a/spec/uploaders/design_management/design_v432x230_uploader_spec.rb
+++ b/spec/uploaders/design_management/design_v432x230_uploader_spec.rb
@@ -11,10 +11,10 @@ RSpec.describe DesignManagement::DesignV432x230Uploader do
subject(:uploader) { described_class.new(model, :image_v432x230) }
it_behaves_like 'builds correct paths',
- store_dir: %r[uploads/-/system/design_management/action/image_v432x230/],
- upload_path: %r[uploads/-/system/design_management/action/image_v432x230/],
- relative_path: %r[uploads/-/system/design_management/action/image_v432x230/],
- absolute_path: %r[#{CarrierWave.root}/uploads/-/system/design_management/action/image_v432x230/]
+ store_dir: %r{uploads/-/system/design_management/action/image_v432x230/},
+ upload_path: %r{uploads/-/system/design_management/action/image_v432x230/},
+ relative_path: %r{uploads/-/system/design_management/action/image_v432x230/},
+ absolute_path: %r{#{CarrierWave.root}/uploads/-/system/design_management/action/image_v432x230/}
context 'object_store is REMOTE' do
before do
@@ -24,9 +24,9 @@ RSpec.describe DesignManagement::DesignV432x230Uploader do
include_context 'with storage', described_class::Store::REMOTE
it_behaves_like 'builds correct paths',
- store_dir: %r[design_management/action/image_v432x230/],
- upload_path: %r[design_management/action/image_v432x230/],
- relative_path: %r[design_management/action/image_v432x230/]
+ store_dir: %r{design_management/action/image_v432x230/},
+ upload_path: %r{design_management/action/image_v432x230/},
+ relative_path: %r{design_management/action/image_v432x230/}
end
describe "#migrate!" do
diff --git a/spec/uploaders/external_diff_uploader_spec.rb b/spec/uploaders/external_diff_uploader_spec.rb
index 2121e9cbc29..25e8bd0a4dc 100644
--- a/spec/uploaders/external_diff_uploader_spec.rb
+++ b/spec/uploaders/external_diff_uploader_spec.rb
@@ -9,9 +9,9 @@ RSpec.describe ExternalDiffUploader do
subject(:uploader) { described_class.new(diff, :external_diff) }
it_behaves_like "builds correct paths",
- store_dir: %r[merge_request_diffs/mr-\d+],
- cache_dir: %r[/external-diffs/tmp/cache],
- work_dir: %r[/external-diffs/tmp/work]
+ store_dir: %r{merge_request_diffs/mr-\d+},
+ cache_dir: %r{/external-diffs/tmp/cache},
+ work_dir: %r{/external-diffs/tmp/work}
context "object store is REMOTE" do
before do
@@ -21,7 +21,7 @@ RSpec.describe ExternalDiffUploader do
include_context 'with storage', described_class::Store::REMOTE
it_behaves_like "builds correct paths",
- store_dir: %r[merge_request_diffs/mr-\d+]
+ store_dir: %r{merge_request_diffs/mr-\d+}
end
describe 'remote file' do
diff --git a/spec/uploaders/import_export_uploader_spec.rb b/spec/uploaders/import_export_uploader_spec.rb
index 64e92f5d60e..1a2041df3d0 100644
--- a/spec/uploaders/import_export_uploader_spec.rb
+++ b/spec/uploaders/import_export_uploader_spec.rb
@@ -39,8 +39,8 @@ RSpec.describe ImportExportUploader do
include_context 'with storage', described_class::Store::REMOTE
patterns = {
- store_dir: %r[import_export_upload/import_file/],
- upload_path: %r[import_export_upload/import_file/]
+ store_dir: %r{import_export_upload/import_file/},
+ upload_path: %r{import_export_upload/import_file/}
}
it_behaves_like 'builds correct paths', patterns do
diff --git a/spec/uploaders/job_artifact_uploader_spec.rb b/spec/uploaders/job_artifact_uploader_spec.rb
index dac9e97641d..ea4f0036fa4 100644
--- a/spec/uploaders/job_artifact_uploader_spec.rb
+++ b/spec/uploaders/job_artifact_uploader_spec.rb
@@ -11,8 +11,8 @@ RSpec.describe JobArtifactUploader do
it_behaves_like "builds correct paths",
store_dir: %r[\h{2}/\h{2}/\h{64}/\d{4}_\d{1,2}_\d{1,2}/\d+/\d+\z],
- cache_dir: %r[artifacts/tmp/cache],
- work_dir: %r[artifacts/tmp/work]
+ cache_dir: %r{artifacts/tmp/cache},
+ work_dir: %r{artifacts/tmp/work}
context "object store is REMOTE" do
before do
diff --git a/spec/uploaders/lfs_object_uploader_spec.rb b/spec/uploaders/lfs_object_uploader_spec.rb
index 9bbfd910ada..77bde5da7ea 100644
--- a/spec/uploaders/lfs_object_uploader_spec.rb
+++ b/spec/uploaders/lfs_object_uploader_spec.rb
@@ -11,8 +11,8 @@ RSpec.describe LfsObjectUploader do
it_behaves_like "builds correct paths",
store_dir: %r[\h{2}/\h{2}],
- cache_dir: %r[/lfs-objects/tmp/cache],
- work_dir: %r[/lfs-objects/tmp/work]
+ cache_dir: %r{/lfs-objects/tmp/cache},
+ work_dir: %r{/lfs-objects/tmp/work}
context "object store is REMOTE" do
before do
diff --git a/spec/uploaders/namespace_file_uploader_spec.rb b/spec/uploaders/namespace_file_uploader_spec.rb
index 02381123ba5..0db7f82dcbd 100644
--- a/spec/uploaders/namespace_file_uploader_spec.rb
+++ b/spec/uploaders/namespace_file_uploader_spec.rb
@@ -13,9 +13,9 @@ RSpec.describe NamespaceFileUploader do
it_behaves_like 'builds correct paths' do
let(:patterns) do
{
- store_dir: %r[uploads/-/system/namespace/\d+],
+ store_dir: %r{uploads/-/system/namespace/\d+},
upload_path: identifier,
- absolute_path: %r[#{CarrierWave.root}/uploads/-/system/namespace/\d+/#{identifier}]
+ absolute_path: %r{#{CarrierWave.root}/uploads/-/system/namespace/\d+/#{identifier}}
}
end
end
@@ -30,7 +30,7 @@ RSpec.describe NamespaceFileUploader do
it_behaves_like 'builds correct paths' do
let(:patterns) do
{
- store_dir: %r[namespace/\d+/\h+],
+ store_dir: %r{namespace/\d+/\h+},
upload_path: identifier
}
end
diff --git a/spec/uploaders/object_storage_spec.rb b/spec/uploaders/object_storage_spec.rb
index 576f6deeec6..e4a9b92df64 100644
--- a/spec/uploaders/object_storage_spec.rb
+++ b/spec/uploaders/object_storage_spec.rb
@@ -255,7 +255,7 @@ RSpec.describe ObjectStorage, :clean_gitlab_redis_shared_state, feature_category
describe '#use_file' do
context 'when file is stored locally' do
it "calls a regular path" do
- expect { |b| uploader.use_file(&b) }.not_to yield_with_args(%r[tmp/cache])
+ expect { |b| uploader.use_file(&b) }.not_to yield_with_args(%r{tmp/cache})
end
end
@@ -267,7 +267,7 @@ RSpec.describe ObjectStorage, :clean_gitlab_redis_shared_state, feature_category
end
it "calls a cache path" do
- expect { |b| uploader.use_file(&b) }.to yield_with_args(%r[tmp/cache])
+ expect { |b| uploader.use_file(&b) }.to yield_with_args(%r{tmp/cache})
end
it "cleans up the cached file" do
diff --git a/spec/uploaders/packages/composer/cache_uploader_spec.rb b/spec/uploaders/packages/composer/cache_uploader_spec.rb
index 7eea4a839ab..56e8b28ef36 100644
--- a/spec/uploaders/packages/composer/cache_uploader_spec.rb
+++ b/spec/uploaders/packages/composer/cache_uploader_spec.rb
@@ -10,8 +10,8 @@ RSpec.describe Packages::Composer::CacheUploader do
it_behaves_like "builds correct paths",
store_dir: %r[^\h{2}/\h{2}/\h{64}/packages/composer_cache/\d+$],
- cache_dir: %r[/packages/tmp/cache],
- work_dir: %r[/packages/tmp/work]
+ cache_dir: %r{/packages/tmp/cache},
+ work_dir: %r{/packages/tmp/work}
context 'object store is remote' do
before do
diff --git a/spec/uploaders/packages/debian/component_file_uploader_spec.rb b/spec/uploaders/packages/debian/component_file_uploader_spec.rb
index 84ba751c737..ffc5d8085fa 100644
--- a/spec/uploaders/packages/debian/component_file_uploader_spec.rb
+++ b/spec/uploaders/packages/debian/component_file_uploader_spec.rb
@@ -13,8 +13,8 @@ RSpec.describe Packages::Debian::ComponentFileUploader do
it_behaves_like "builds correct paths",
store_dir: %r[^\h{2}/\h{2}/\h{64}/debian_#{container_type}_component_file/\d+$],
- cache_dir: %r[/packages/tmp/cache$],
- work_dir: %r[/packages/tmp/work$]
+ cache_dir: %r{/packages/tmp/cache$},
+ work_dir: %r{/packages/tmp/work$}
context 'object store is remote' do
before do
@@ -25,8 +25,8 @@ RSpec.describe Packages::Debian::ComponentFileUploader do
it_behaves_like "builds correct paths",
store_dir: %r[^\h{2}/\h{2}/\h{64}/debian_#{container_type}_component_file/\d+$],
- cache_dir: %r[/packages/tmp/cache$],
- work_dir: %r[/packages/tmp/work$]
+ cache_dir: %r{/packages/tmp/cache$},
+ work_dir: %r{/packages/tmp/work$}
end
describe 'remote file' do
diff --git a/spec/uploaders/packages/debian/distribution_release_file_uploader_spec.rb b/spec/uploaders/packages/debian/distribution_release_file_uploader_spec.rb
index df630569856..2086ab5966c 100644
--- a/spec/uploaders/packages/debian/distribution_release_file_uploader_spec.rb
+++ b/spec/uploaders/packages/debian/distribution_release_file_uploader_spec.rb
@@ -13,8 +13,8 @@ RSpec.describe Packages::Debian::DistributionReleaseFileUploader do
it_behaves_like "builds correct paths",
store_dir: %r[^\h{2}/\h{2}/\h{64}/debian_#{container_type}_distribution/\d+$],
- cache_dir: %r[/packages/tmp/cache$],
- work_dir: %r[/packages/tmp/work$]
+ cache_dir: %r{/packages/tmp/cache$},
+ work_dir: %r{/packages/tmp/work$}
context 'object store is remote' do
before do
@@ -25,8 +25,8 @@ RSpec.describe Packages::Debian::DistributionReleaseFileUploader do
it_behaves_like "builds correct paths",
store_dir: %r[^\h{2}/\h{2}/\h{64}/debian_#{container_type}_distribution/\d+$],
- cache_dir: %r[/packages/tmp/cache$],
- work_dir: %r[/packages/tmp/work$]
+ cache_dir: %r{/packages/tmp/cache$},
+ work_dir: %r{/packages/tmp/work$}
end
describe 'remote file' do
diff --git a/spec/uploaders/packages/package_file_uploader_spec.rb b/spec/uploaders/packages/package_file_uploader_spec.rb
index ddd9823d55c..36acb681669 100644
--- a/spec/uploaders/packages/package_file_uploader_spec.rb
+++ b/spec/uploaders/packages/package_file_uploader_spec.rb
@@ -10,8 +10,8 @@ RSpec.describe Packages::PackageFileUploader do
it_behaves_like "builds correct paths",
store_dir: %r[^\h{2}/\h{2}/\h{64}/packages/\d+/files/\d+$],
- cache_dir: %r[/packages/tmp/cache],
- work_dir: %r[/packages/tmp/work]
+ cache_dir: %r{/packages/tmp/cache},
+ work_dir: %r{/packages/tmp/work}
context 'object store is remote' do
before do
diff --git a/spec/uploaders/pages/deployment_uploader_spec.rb b/spec/uploaders/pages/deployment_uploader_spec.rb
index 7686efd4fe4..a5fe2dfe9ba 100644
--- a/spec/uploaders/pages/deployment_uploader_spec.rb
+++ b/spec/uploaders/pages/deployment_uploader_spec.rb
@@ -14,8 +14,8 @@ RSpec.describe Pages::DeploymentUploader do
it_behaves_like "builds correct paths",
store_dir: %r[/\h{2}/\h{2}/\h{64}/pages_deployments/\d+],
- cache_dir: %r[pages/@hashed/tmp/cache],
- work_dir: %r[pages/@hashed/tmp/work]
+ cache_dir: %r{pages/@hashed/tmp/cache},
+ work_dir: %r{pages/@hashed/tmp/work}
context 'when object store is REMOTE' do
before do
diff --git a/spec/uploaders/personal_file_uploader_spec.rb b/spec/uploaders/personal_file_uploader_spec.rb
index 58edf3f093d..de5ed8318e4 100644
--- a/spec/uploaders/personal_file_uploader_spec.rb
+++ b/spec/uploaders/personal_file_uploader_spec.rb
@@ -43,16 +43,16 @@ RSpec.describe PersonalFileUploader do
it 'builds correct paths for both local and remote storage' do
paths = uploader.upload_paths('test.jpg')
- expect(paths.first).to match(%r[\h+/test.jpg])
- expect(paths.second).to match(%r[^personal_snippet/\d+/\h+/test.jpg])
+ expect(paths.first).to match(%r{\h+/test.jpg})
+ expect(paths.second).to match(%r{^personal_snippet/\d+/\h+/test.jpg})
end
end
context 'object_store is LOCAL' do
it_behaves_like 'builds correct paths',
- store_dir: %r[uploads/-/system/personal_snippet/\d+/\h+],
- upload_path: %r[\h+/\S+],
- absolute_path: %r[#{CarrierWave.root}/uploads/-/system/personal_snippet/\d+/\h+/\S+$]
+ store_dir: %r{uploads/-/system/personal_snippet/\d+/\h+},
+ upload_path: %r{\h+/\S+},
+ absolute_path: %r{#{CarrierWave.root}/uploads/-/system/personal_snippet/\d+/\h+/\S+$}
it_behaves_like '#base_dir'
it_behaves_like '#to_h'
@@ -66,8 +66,8 @@ RSpec.describe PersonalFileUploader do
include_context 'with storage', described_class::Store::REMOTE
it_behaves_like 'builds correct paths',
- store_dir: %r[\d+/\h+],
- upload_path: %r[^personal_snippet/\d+/\h+/<filename>]
+ store_dir: %r{\d+/\h+},
+ upload_path: %r{^personal_snippet/\d+/\h+/<filename>}
it_behaves_like '#base_dir'
it_behaves_like '#to_h'
diff --git a/spec/validators/any_field_validator_spec.rb b/spec/validators/any_field_validator_spec.rb
index bede006abf6..2d3d3982828 100644
--- a/spec/validators/any_field_validator_spec.rb
+++ b/spec/validators/any_field_validator_spec.rb
@@ -8,7 +8,7 @@ RSpec.describe AnyFieldValidator do
Class.new(ApplicationRecord) do
self.table_name = 'vulnerabilities'
- validates_with AnyFieldValidator, fields: %w(title description)
+ validates_with AnyFieldValidator, fields: %w[title description]
end
end
@@ -18,7 +18,7 @@ RSpec.describe AnyFieldValidator do
expect(validated_object.valid?).to be_falsey
expect(validated_object.errors.messages)
.to eq(base: ["At least one field of %{one_of_required_fields} must be present" %
- { one_of_required_fields: %w(title description) }])
+ { one_of_required_fields: %w[title description] }])
end
it 'validates if only one field is present' do
diff --git a/spec/validators/ip_cidr_array_validator_spec.rb b/spec/validators/ip_cidr_array_validator_spec.rb
new file mode 100644
index 00000000000..4ea46d714c2
--- /dev/null
+++ b/spec/validators/ip_cidr_array_validator_spec.rb
@@ -0,0 +1,45 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe IpCidrArrayValidator, feature_category: :shared do
+ let(:model) do
+ Class.new do
+ include ActiveModel::Model
+ include ActiveModel::Validations
+
+ attr_accessor :cidr_array
+ alias_method :cidr_array_before_type_cast, :cidr_array
+
+ validates :cidr_array, ip_cidr_array: true
+ end.new
+ end
+
+ using RSpec::Parameterized::TableSyntax
+
+ # noinspection RubyMismatchedArgumentType - RubyMine is resolving `#|` from Array, instead of Rspec::Parameterized
+ where(:cidr_array, :validity, :errors) do
+ # rubocop:disable Layout/LineLength -- The RSpec table syntax often requires long lines for errors
+ nil | false | { cidr_array: ["must be an array of CIDR values"] }
+ '' | false | { cidr_array: ["must be an array of CIDR values"] }
+ ['172.0.0.1/256'] | false | { cidr_array: ["IP '172.0.0.1/256' is not a valid CIDR: Invalid netmask 256"] }
+ %w[172.0.0.1/24 invalid-CIDR] | false | { cidr_array: ["IP 'invalid-CIDR' is not a valid CIDR: IP should be followed by a slash followed by an integer subnet mask (for example: '192.168.1.0/24')"] }
+ %w[172.0.0.1/256 invalid-CIDR] | false | { cidr_array: ["IP '172.0.0.1/256' is not a valid CIDR: Invalid netmask 256", "IP 'invalid-CIDR' is not a valid CIDR: IP should be followed by a slash followed by an integer subnet mask (for example: '192.168.1.0/24')"] }
+ ['172.0.0.1/24', nil] | true | {}
+ %w[172.0.0.1/24 2001:db8::8:800:200c:417a/128] | true | {}
+ [] | true | {}
+ [nil] | true | {}
+ [''] | true | {}
+ # rubocop:enable Layout/LineLength
+ end
+
+ with_them do
+ before do
+ model.cidr_array = cidr_array
+ model.validate
+ end
+
+ it { expect(model.valid?).to eq(validity) }
+ it { expect(model.errors.messages).to eq(errors) }
+ end
+end
diff --git a/spec/validators/ip_cidr_validator_spec.rb b/spec/validators/ip_cidr_validator_spec.rb
new file mode 100644
index 00000000000..213991d9f4f
--- /dev/null
+++ b/spec/validators/ip_cidr_validator_spec.rb
@@ -0,0 +1,45 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe IpCidrValidator, feature_category: :shared do
+ let(:model) do
+ Class.new do
+ include ActiveModel::Model
+ include ActiveModel::Validations
+
+ attr_accessor :cidr
+ alias_method :cidr_before_type_cast, :cidr
+
+ validates :cidr, ip_cidr: true
+ end.new
+ end
+
+ using RSpec::Parameterized::TableSyntax
+
+ where(:cidr, :validity, :errors) do
+ # rubocop:disable Layout/LineLength -- The RSpec table syntax often requires long lines for errors'
+ 'invalid-CIDR' | false | { cidr: ["IP 'invalid-CIDR' is not a valid CIDR: IP should be followed by a slash followed by an integer subnet mask (for example: '192.168.1.0/24')"] }
+ '172.0.0.1|256' | false | { cidr: ["IP '172.0.0.1|256' is not a valid CIDR: IP should be followed by a slash followed by an integer subnet mask (for example: '192.168.1.0/24')"] }
+ '172.0.0.1' | false | { cidr: ["IP '172.0.0.1' is not a valid CIDR: IP should be followed by a slash followed by an integer subnet mask (for example: '192.168.1.0/24')"] }
+ '172.0.0.1/2/12' | false | { cidr: ["IP '172.0.0.1/2/12' is not a valid CIDR: IP should be followed by a slash followed by an integer subnet mask (for example: '192.168.1.0/24')"] }
+ '172.0.0.1/256' | false | { cidr: ["IP '172.0.0.1/256' is not a valid CIDR: Invalid netmask 256"] }
+ '2001:db8::8:800:200c:417a/129' | false | { cidr: ["IP '2001:db8::8:800:200c:417a/129' is not a valid CIDR: Prefix must be in range 0..128, got: 129"] }
+ '2001:db8::8:800:200c:417a' | false | { cidr: ["IP '2001:db8::8:800:200c:417a' is not a valid CIDR: IP should be followed by a slash followed by an integer subnet mask (for example: '192.168.1.0/24')"] }
+ '2001:db8::8:800:200c:417a/128' | true | {}
+ '172.0.0.1/32' | true | {}
+ '' | true | {}
+ nil | true | {}
+ # rubocop:enable Layout/LineLength
+ end
+
+ with_them do
+ before do
+ model.cidr = cidr
+ model.validate
+ end
+
+ it { expect(model.valid?).to eq(validity) }
+ it { expect(model.errors.messages).to eq(errors) }
+ end
+end
diff --git a/spec/views/admin/application_settings/_repository_storage.html.haml_spec.rb b/spec/views/admin/application_settings/_repository_storage.html.haml_spec.rb
index 244157a3b14..34821149444 100644
--- a/spec/views/admin/application_settings/_repository_storage.html.haml_spec.rb
+++ b/spec/views/admin/application_settings/_repository_storage.html.haml_spec.rb
@@ -6,7 +6,7 @@ RSpec.describe 'admin/application_settings/_repository_storage.html.haml' do
let(:app_settings) { build(:application_setting, repository_storages_weighted: repository_storages_weighted) }
before do
- stub_storage_settings({ 'default': {}, 'mepmep': {}, 'foobar': {} })
+ stub_storage_settings({ default: {}, mepmep: {}, foobar: {} })
assign(:application_setting, app_settings)
end
diff --git a/spec/views/ci/status/_badge.html.haml_spec.rb b/spec/views/ci/status/_badge.html.haml_spec.rb
deleted file mode 100644
index 65497de1608..00000000000
--- a/spec/views/ci/status/_badge.html.haml_spec.rb
+++ /dev/null
@@ -1,92 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe 'ci/status/_badge' do
- let(:user) { create(:user) }
- let(:project) { create(:project, :private) }
- let(:pipeline) { create(:ci_pipeline, project: project) }
-
- context 'when rendering status for build' do
- let(:build) do
- create(:ci_build, :success, pipeline: pipeline)
- end
-
- context 'when user has ability to see details' do
- before do
- project.add_developer(user)
- end
-
- it 'has link to build details page' do
- details_path = project_job_path(project, build)
-
- render_status(build)
-
- expect(rendered).to have_link 'Passed', href: details_path
- end
- end
-
- context 'when user do not have ability to see build details' do
- before do
- render_status(build)
- end
-
- it 'contains build status text' do
- expect(rendered).to have_content 'Passed'
- end
-
- it 'does not contain links' do
- expect(rendered).not_to have_link 'Passed'
- end
- end
- end
-
- context 'when rendering status for external job' do
- context 'when user has ability to see commit status details' do
- before do
- project.add_developer(user)
- end
-
- context 'status has external target url' do
- before do
- external_job = create(
- :generic_commit_status,
- status: :running,
- pipeline: pipeline,
- target_url: 'http://gitlab.com'
- )
-
- render_status(external_job)
- end
-
- it 'contains valid commit status text' do
- expect(rendered).to have_content 'Running'
- end
-
- it 'has link to external status page' do
- expect(rendered).to have_link 'Running', href: 'http://gitlab.com'
- end
- end
-
- context 'status do not have external target url' do
- before do
- external_job = create(:generic_commit_status, status: :canceled)
-
- render_status(external_job)
- end
-
- it 'contains valid commit status text' do
- expect(rendered).to have_content 'Canceled'
- end
-
- it 'has link to external status page' do
- expect(rendered).not_to have_link 'Canceled'
- end
- end
- end
- end
-
- def render_status(resource)
- render 'ci/status/badge', status: resource.detailed_status(user)
- end
-end
diff --git a/spec/views/ci/status/_icon.html.haml_spec.rb b/spec/views/ci/status/_icon.html.haml_spec.rb
index 78b19957cf0..a3058b20255 100644
--- a/spec/views/ci/status/_icon.html.haml_spec.rb
+++ b/spec/views/ci/status/_icon.html.haml_spec.rb
@@ -31,7 +31,7 @@ RSpec.describe 'ci/status/_icon' do
end
it 'contains build status text' do
- expect(rendered).to have_css('.ci-status-icon.ci-status-icon-success')
+ expect(rendered).to have_css('[data-testid="status_success_borderless-icon"]')
end
it 'does not contain links' do
@@ -59,7 +59,7 @@ RSpec.describe 'ci/status/_icon' do
end
it 'contains valid commit status text' do
- expect(rendered).to have_css('.ci-status-icon.ci-status-icon-running')
+ expect(rendered).to have_css('[data-testid="status_running_borderless-icon"]')
end
it 'has link to external status page' do
@@ -75,7 +75,7 @@ RSpec.describe 'ci/status/_icon' do
end
it 'contains valid commit status text' do
- expect(rendered).to have_css('.ci-status-icon.ci-status-icon-canceled')
+ expect(rendered).to have_css('[data-testid="status_canceled_borderless-icon"]')
end
it 'has link to external status page' do
diff --git a/spec/views/groups/edit.html.haml_spec.rb b/spec/views/groups/edit.html.haml_spec.rb
index 4c49f1529f2..f33a4190bf8 100644
--- a/spec/views/groups/edit.html.haml_spec.rb
+++ b/spec/views/groups/edit.html.haml_spec.rb
@@ -7,7 +7,6 @@ RSpec.describe 'groups/edit.html.haml', feature_category: :groups_and_projects d
before do
stub_template 'groups/settings/_code_suggestions' => ''
- stub_template 'groups/settings/_ai_third_party_settings' => ''
end
describe '"Share with group lock" setting' do
diff --git a/spec/views/layouts/_head.html.haml_spec.rb b/spec/views/layouts/_head.html.haml_spec.rb
index 504a9492d7a..56936dbafcf 100644
--- a/spec/views/layouts/_head.html.haml_spec.rb
+++ b/spec/views/layouts/_head.html.haml_spec.rb
@@ -15,7 +15,7 @@ RSpec.describe 'layouts/_head' do
render
- expect(rendered).to match(%{content="foo&quot; http-equiv=&quot;refresh"})
+ expect(rendered).to match(%(content="foo&quot; http-equiv=&quot;refresh"))
end
it 'escapes HTML-safe strings in page_description' do
@@ -23,7 +23,7 @@ RSpec.describe 'layouts/_head' do
render
- expect(rendered).to match(%{content="foo&quot; http-equiv=&quot;refresh"})
+ expect(rendered).to match(%(content="foo&quot; http-equiv=&quot;refresh"))
end
it 'escapes HTML-safe strings in page_image' do
@@ -31,7 +31,7 @@ RSpec.describe 'layouts/_head' do
render
- expect(rendered).to match(%{content="foo&quot; http-equiv=&quot;refresh"})
+ expect(rendered).to match(%(content="foo&quot; http-equiv=&quot;refresh"))
end
context 'when an asset_host is set' do
@@ -101,7 +101,7 @@ RSpec.describe 'layouts/_head' do
render
expect(rendered).to match(%r{<script.*>.*var u="//#{matomo_host}/".*</script>}m)
- expect(rendered).to match(%r(<noscript>.*<img src="//#{matomo_host}/matomo.php.*</noscript>))
+ expect(rendered).to match(%r{<noscript>.*<img src="//#{matomo_host}/matomo.php.*</noscript>})
expect(rendered).not_to include('_paq.push(["disableCookies"])')
end
@@ -120,6 +120,6 @@ RSpec.describe 'layouts/_head' do
def stub_helper_with_safe_string(method)
allow_any_instance_of(PageLayoutHelper).to receive(method)
- .and_return(%q{foo" http-equiv="refresh}.html_safe)
+ .and_return(%q(foo" http-equiv="refresh).html_safe)
end
end
diff --git a/spec/views/layouts/application.html.haml_spec.rb b/spec/views/layouts/application.html.haml_spec.rb
index 825e295b73d..20bef2a3685 100644
--- a/spec/views/layouts/application.html.haml_spec.rb
+++ b/spec/views/layouts/application.html.haml_spec.rb
@@ -80,7 +80,6 @@ RSpec.describe 'layouts/application' do
before do
allow(view).to receive(:current_user).and_return(nil)
allow(view).to receive(:current_user_mode).and_return(Gitlab::Auth::CurrentUserMode.new(nil))
- Feature.enable(:super_sidebar_logged_out)
end
it 'renders the new marketing header for logged-out users' do
diff --git a/spec/views/layouts/header/_new_dropdown.haml_spec.rb b/spec/views/layouts/header/_new_dropdown.haml_spec.rb
index 2c5882fce3d..ef028da7ab9 100644
--- a/spec/views/layouts/header/_new_dropdown.haml_spec.rb
+++ b/spec/views/layouts/header/_new_dropdown.haml_spec.rb
@@ -185,6 +185,11 @@ RSpec.describe 'layouts/header/_new_dropdown', feature_category: :navigation do
context 'when the user is not allowed to do anything' do
let(:user) { create(:user, :external) } # rubocop:disable RSpec/FactoryBot/AvoidCreate
+ before do
+ allow(user).to receive(:can?).and_call_original
+ allow(user).to receive(:can?).with(:create_organization).and_return(false)
+ end
+
it 'is nil' do
# We have to use `view.render` because `render` causes issues
# https://github.com/rails/rails/issues/41320
diff --git a/spec/views/layouts/header/_super_sidebar_logged_out.html.haml_spec.rb b/spec/views/layouts/header/_super_sidebar_logged_out.html.haml_spec.rb
index f81e8c5badf..7f49f96de03 100644
--- a/spec/views/layouts/header/_super_sidebar_logged_out.html.haml_spec.rb
+++ b/spec/views/layouts/header/_super_sidebar_logged_out.html.haml_spec.rb
@@ -5,7 +5,6 @@ require 'spec_helper'
RSpec.describe 'layouts/header/_super_sidebar_logged_out', feature_category: :navigation do
before do
allow(view).to receive(:current_user_mode).and_return(Gitlab::Auth::CurrentUserMode.new(nil))
- Feature.enable(:super_sidebar_logged_out)
end
context 'on gitlab.com' do
diff --git a/spec/views/layouts/nav/sidebar/_project.html.haml_spec.rb b/spec/views/layouts/nav/sidebar/_project.html.haml_spec.rb
index 3ec731c8eb7..34debcab5f7 100644
--- a/spec/views/layouts/nav/sidebar/_project.html.haml_spec.rb
+++ b/spec/views/layouts/nav/sidebar/_project.html.haml_spec.rb
@@ -23,7 +23,7 @@ RSpec.describe 'layouts/nav/sidebar/_project', feature_category: :navigation do
it 'has a link to the project path' do
render
- expect(rendered).to have_link(project.name, href: project_path(project), class: %w(shortcuts-project rspec-project-link))
+ expect(rendered).to have_link(project.name, href: project_path(project), class: 'shortcuts-project')
expect(rendered).to have_selector("[aria-label=\"#{project.name}\"]")
end
end
@@ -32,7 +32,7 @@ RSpec.describe 'layouts/nav/sidebar/_project', feature_category: :navigation do
it 'has a link to the project activity path' do
render
- expect(rendered).to have_link('Project information', href: activity_project_path(project), class: %w(shortcuts-project-information))
+ expect(rendered).to have_link('Project information', href: activity_project_path(project), class: %w[shortcuts-project-information])
expect(rendered).to have_selector('[aria-label="Project information"]')
end
diff --git a/spec/views/layouts/snippets.html.haml_spec.rb b/spec/views/layouts/snippets.html.haml_spec.rb
deleted file mode 100644
index b7139f84174..00000000000
--- a/spec/views/layouts/snippets.html.haml_spec.rb
+++ /dev/null
@@ -1,32 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe 'layouts/snippets', feature_category: :source_code_management do
- before do
- allow(view).to receive(:current_user).and_return(user)
- allow(view).to receive(:current_user_mode).and_return(Gitlab::Auth::CurrentUserMode.new(user))
- end
-
- describe 'sidebar' do
- context 'when signed in' do
- let(:user) { build_stubbed(:user, :no_super_sidebar) }
-
- it 'renders the "Your work" sidebar' do
- render
-
- expect(rendered).to have_css('aside.nav-sidebar[aria-label="Your work"]')
- end
- end
-
- context 'when not signed in' do
- let(:user) { nil }
-
- it 'renders no sidebar' do
- render
-
- expect(rendered).not_to have_css('aside.nav-sidebar')
- end
- end
- end
-end
diff --git a/spec/views/projects/commit/branches.html.haml_spec.rb b/spec/views/projects/commit/branches.html.haml_spec.rb
index f1064be3047..d6fbf6453c0 100644
--- a/spec/views/projects/commit/branches.html.haml_spec.rb
+++ b/spec/views/projects/commit/branches.html.haml_spec.rb
@@ -61,7 +61,7 @@ RSpec.describe 'projects/commit/branches.html.haml' do
before do
assign(:branches, ['master'])
assign(:branches_limit_exceeded, true)
- assign(:tags, %w(tag1 tag2))
+ assign(:tags, %w[tag1 tag2])
assign(:tags_limit_exceeded, false)
render
diff --git a/spec/views/projects/commits/_commit.html.haml_spec.rb b/spec/views/projects/commits/_commit.html.haml_spec.rb
index d45f1da86e8..cc73418ea1e 100644
--- a/spec/views/projects/commits/_commit.html.haml_spec.rb
+++ b/spec/views/projects/commits/_commit.html.haml_spec.rb
@@ -74,7 +74,7 @@ RSpec.describe 'projects/commits/_commit.html.haml' do
commit: commit
}
- expect(rendered).not_to have_css("[data-testid='ci-status-badge-legacy']")
+ expect(rendered).not_to have_css("[data-testid='ci-icon']")
end
end
@@ -91,7 +91,7 @@ RSpec.describe 'projects/commits/_commit.html.haml' do
commit: commit
}
- expect(rendered).to have_css("[data-testid='ci-status-badge-legacy']")
+ expect(rendered).to have_css("[data-testid='ci-icon']")
end
end
@@ -103,7 +103,7 @@ RSpec.describe 'projects/commits/_commit.html.haml' do
commit: commit
}
- expect(rendered).not_to have_css("[data-testid='ci-status-badge-legacy']")
+ expect(rendered).not_to have_css("[data-testid='ci-icon']")
end
end
end
diff --git a/spec/views/projects/issues/_related_branches.html.haml_spec.rb b/spec/views/projects/issues/_related_branches.html.haml_spec.rb
index deec2db6865..d37c8c762a3 100644
--- a/spec/views/projects/issues/_related_branches.html.haml_spec.rb
+++ b/spec/views/projects/issues/_related_branches.html.haml_spec.rb
@@ -25,8 +25,7 @@ RSpec.describe 'projects/issues/_related_branches' do
expect(rendered).to have_text('other')
expect(rendered).to have_link(href: 'link-to-feature')
expect(rendered).to have_link(href: 'link-to-other')
- expect(rendered).to have_css('.related-branch-ci-status')
- expect(rendered).to have_css('.ci-status-icon')
+ expect(rendered).to have_css('[data-testid="ci-icon"]')
expect(rendered).to have_css('.related-branch-info')
end
end
diff --git a/spec/views/projects/pages/new.html.haml_spec.rb b/spec/views/projects/pages/new.html.haml_spec.rb
index 919b2fe84ee..d7295d60c51 100644
--- a/spec/views/projects/pages/new.html.haml_spec.rb
+++ b/spec/views/projects/pages/new.html.haml_spec.rb
@@ -13,30 +13,8 @@ RSpec.describe 'projects/pages/new' do
allow(view).to receive(:current_user).and_return(user)
end
- describe 'with onboarding wizard feature enabled' do
- before do
- Feature.enable(:use_pipeline_wizard_for_pages)
- end
-
- it "shows the onboarding wizard" do
- render
- expect(rendered).to have_selector('#js-pages')
- end
- end
-
- describe 'with onboarding wizard feature disabled' do
- before do
- Feature.disable(:use_pipeline_wizard_for_pages)
- end
-
- it "does not show the onboarding wizard" do
- render
- expect(rendered).not_to have_selector('#js-pages')
- end
-
- it "renders the usage instructions" do
- render
- expect(rendered).to render_template('projects/pages/_use')
- end
+ it "shows the onboarding wizard" do
+ render
+ expect(rendered).to have_selector('#js-pages')
end
end
diff --git a/spec/views/projects/tags/index.html.haml_spec.rb b/spec/views/projects/tags/index.html.haml_spec.rb
index 01e8d23fb9f..0ac5efa2e6d 100644
--- a/spec/views/projects/tags/index.html.haml_spec.rb
+++ b/spec/views/projects/tags/index.html.haml_spec.rb
@@ -91,7 +91,7 @@ RSpec.describe 'projects/tags/index.html.haml' do
render
- expect(page.find('.tags .content-list li', text: tag)).to have_css '.gl-badge .ci-status-icon-success'
+ expect(page.find('.tags .content-list li', text: tag)).to have_css '[data-testid="status_success_borderless-icon"]'
expect(page.all('.tags .content-list li')).to all(have_css('svg.s16'))
end
diff --git a/spec/views/search/show.html.haml_spec.rb b/spec/views/search/show.html.haml_spec.rb
index 0158a9049b9..e6d2ef02c9a 100644
--- a/spec/views/search/show.html.haml_spec.rb
+++ b/spec/views/search/show.html.haml_spec.rb
@@ -29,13 +29,6 @@ RSpec.describe 'search/show', feature_category: :global_search do
end
context 'when the search page is opened' do
- it 'displays the title' do
- render
-
- expect(rendered).to have_selector('h1.page-title', text: 'Search')
- expect(rendered).not_to have_selector('h1.page-title code')
- end
-
it 'does not render the results partial' do
render
diff --git a/spec/workers/abuse/spam_abuse_events_worker_spec.rb b/spec/workers/abuse/spam_abuse_events_worker_spec.rb
new file mode 100644
index 00000000000..9198636e114
--- /dev/null
+++ b/spec/workers/abuse/spam_abuse_events_worker_spec.rb
@@ -0,0 +1,85 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Abuse::SpamAbuseEventsWorker, :clean_gitlab_redis_shared_state, feature_category: :instance_resiliency do
+ let(:worker) { described_class.new }
+ let_it_be(:user) { create(:user) }
+
+ let(:params) do
+ {
+ user_id: user.id,
+ title: 'Test title',
+ description: 'Test description',
+ source_ip: '1.2.3.4',
+ user_agent: 'fake-user-agent',
+ noteable_type: 'Issue',
+ verdict: 'BLOCK_USER'
+ }
+ end
+
+ shared_examples 'creates an abuse event with the correct data' do
+ it do
+ expect { worker.perform(params) }.to change { Abuse::Event.count }.from(0).to(1)
+ expect(Abuse::Event.last.attributes).to include({
+ abuse_report_id: report_id,
+ category: "spam",
+ metadata: params.except(:user_id),
+ source: "spamcheck",
+ user_id: params[:user_id]
+ }.deep_stringify_keys)
+ end
+ end
+
+ it_behaves_like 'an idempotent worker' do
+ let(:job_args) { [params] }
+ end
+
+ context "when the user does not exist" do
+ let(:log_payload) { { 'message' => 'User not found.', 'user_id' => user.id } }
+
+ before do
+ allow(User).to receive(:find_by_id).with(user.id).and_return(nil)
+ end
+
+ it 'logs an error' do
+ expect(Sidekiq.logger).to receive(:info).with(hash_including(log_payload))
+
+ expect { worker.perform(params) }.not_to raise_exception
+ end
+
+ it 'does not report the user' do
+ expect(described_class).not_to receive(:report_user).with(user.id)
+
+ worker.perform(params)
+ end
+ end
+
+ context "when the user exists" do
+ context 'and there is an existing abuse report' do
+ let_it_be(:abuse_report) do
+ create(:abuse_report, user: user, reporter: Users::Internal.security_bot, message: 'Test report')
+ end
+
+ it_behaves_like 'creates an abuse event with the correct data' do
+ let(:report_id) { abuse_report.id }
+ end
+ end
+
+ context 'and there is no existing abuse report' do
+ it 'creates an abuse report with the correct data' do
+ expect { worker.perform(params) }.to change { AbuseReport.count }.from(0).to(1)
+ expect(AbuseReport.last.attributes).to include({
+ reporter_id: Users::Internal.security_bot.id,
+ user_id: user.id,
+ category: "spam",
+ message: "User reported for abuse based on spam verdict"
+ }.stringify_keys)
+ end
+
+ it_behaves_like 'creates an abuse event with the correct data' do
+ let(:report_id) { AbuseReport.last.attributes["id"] }
+ end
+ end
+ end
+end
diff --git a/spec/workers/activity_pub/projects/releases_subscription_worker_spec.rb b/spec/workers/activity_pub/projects/releases_subscription_worker_spec.rb
new file mode 100644
index 00000000000..c41c1bb8e1c
--- /dev/null
+++ b/spec/workers/activity_pub/projects/releases_subscription_worker_spec.rb
@@ -0,0 +1,128 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe ActivityPub::Projects::ReleasesSubscriptionWorker, feature_category: :release_orchestration do
+ describe '#perform' do
+ let(:worker) { described_class.new }
+ let(:project) { build_stubbed :project, :public }
+ let(:subscription) { build_stubbed :activity_pub_releases_subscription, project: project }
+ let(:inbox_resolver_service) { instance_double('ActivityPub::InboxResolverService', execute: true) }
+ let(:accept_follow_service) { instance_double('ActivityPub::AcceptFollowService', execute: true) }
+
+ before do
+ allow(ActivityPub::ReleasesSubscription).to receive(:find_by_id) { subscription }
+ allow(subscription).to receive(:destroy).and_return(true)
+ allow(ActivityPub::InboxResolverService).to receive(:new) { inbox_resolver_service }
+ allow(ActivityPub::AcceptFollowService).to receive(:new) { accept_follow_service }
+ end
+
+ context 'when the project is public' do
+ before do
+ worker.perform(subscription.id)
+ end
+
+ context 'when inbox url has not been resolved yet' do
+ it 'calls the service to resolve the inbox url' do
+ expect(inbox_resolver_service).to have_received(:execute)
+ end
+
+ it 'calls the service to send out the Accept activity' do
+ expect(accept_follow_service).to have_received(:execute)
+ end
+ end
+
+ context 'when inbox url has been resolved' do
+ context 'when shared inbox url has not been resolved' do
+ let(:subscription) { build_stubbed :activity_pub_releases_subscription, :inbox, project: project }
+
+ it 'calls the service to resolve the inbox url' do
+ expect(inbox_resolver_service).to have_received(:execute)
+ end
+
+ it 'calls the service to send out the Accept activity' do
+ expect(accept_follow_service).to have_received(:execute)
+ end
+ end
+
+ context 'when shared inbox url has been resolved' do
+ let(:subscription) do
+ build_stubbed :activity_pub_releases_subscription, :inbox, :shared_inbox, project: project
+ end
+
+ it 'does not call the service to resolve the inbox url' do
+ expect(inbox_resolver_service).not_to have_received(:execute)
+ end
+
+ it 'calls the service to send out the Accept activity' do
+ expect(accept_follow_service).to have_received(:execute)
+ end
+ end
+ end
+ end
+
+ shared_examples 'failed job' do
+ it 'does not resolve inbox url' do
+ expect(inbox_resolver_service).not_to have_received(:execute)
+ end
+
+ it 'does not send out Accept activity' do
+ expect(accept_follow_service).not_to have_received(:execute)
+ end
+ end
+
+ context 'when the subscription does not exist' do
+ before do
+ allow(ActivityPub::ReleasesSubscription).to receive(:find_by_id).and_return(nil)
+ worker.perform(subscription.id)
+ end
+
+ it_behaves_like 'failed job'
+ end
+
+ shared_examples 'non public project' do
+ it_behaves_like 'failed job'
+
+ it 'deletes the subscription' do
+ expect(subscription).to have_received(:destroy)
+ end
+ end
+
+ context 'when project has changed to internal' do
+ before do
+ worker.perform(subscription.id)
+ end
+
+ let(:project) { build_stubbed :project, :internal }
+
+ it_behaves_like 'non public project'
+ end
+
+ context 'when project has changed to private' do
+ before do
+ worker.perform(subscription.id)
+ end
+
+ let(:project) { build_stubbed :project, :private }
+
+ it_behaves_like 'non public project'
+ end
+ end
+
+ describe '#sidekiq_retries_exhausted' do
+ let(:project) { build_stubbed :project, :public }
+ let(:subscription) { build_stubbed :activity_pub_releases_subscription, project: project }
+ let(:job) { { 'args' => [project.id, subscription.id], 'error_message' => 'Error' } }
+
+ before do
+ allow(Project).to receive(:find) { project }
+ allow(ActivityPub::ReleasesSubscription).to receive(:find_by_id) { subscription }
+ end
+
+ it 'delete the subscription' do
+ expect(subscription).to receive(:destroy)
+
+ described_class.sidekiq_retries_exhausted_block.call(job, StandardError.new)
+ end
+ end
+end
diff --git a/spec/workers/bulk_import_worker_spec.rb b/spec/workers/bulk_import_worker_spec.rb
index 8b73549e071..2983902dff7 100644
--- a/spec/workers/bulk_import_worker_spec.rb
+++ b/spec/workers/bulk_import_worker_spec.rb
@@ -28,5 +28,19 @@ RSpec.describe BulkImportWorker, feature_category: :importers do
end
end
end
+
+ it_behaves_like 'an idempotent worker'
+ end
+
+ describe '#sidekiq_retries_exhausted' do
+ it 'logs export failure and marks entity as failed' do
+ exception = StandardError.new('Exhausted error!')
+
+ expect(Gitlab::ErrorTracking).to receive(:track_exception).with(exception, bulk_import_id: bulk_import.id)
+
+ described_class.sidekiq_retries_exhausted_block.call({ 'args' => job_args }, exception)
+
+ expect(bulk_import.reload.failed?).to eq(true)
+ end
end
end
diff --git a/spec/workers/bulk_imports/entity_worker_spec.rb b/spec/workers/bulk_imports/entity_worker_spec.rb
index 5f948906c08..690555aa08f 100644
--- a/spec/workers/bulk_imports/entity_worker_spec.rb
+++ b/spec/workers/bulk_imports/entity_worker_spec.rb
@@ -34,7 +34,7 @@ RSpec.describe BulkImports::EntityWorker, feature_category: :importers do
end
it 'enqueues the pipeline workers of the first stage and then re-enqueues itself' do
- expect_next_instance_of(Gitlab::Import::Logger) do |logger|
+ expect_next_instance_of(BulkImports::Logger) do |logger|
expect(logger).to receive(:info).with(hash_including('message' => 'Stage starting', 'entity_stage' => 0))
expect(logger).to receive(:info).with(hash_including('message' => 'Stage running', 'entity_stage' => 0))
end
@@ -49,13 +49,17 @@ RSpec.describe BulkImports::EntityWorker, feature_category: :importers do
end
end
+ it 'has the option to reschedule once if deduplicated' do
+ expect(described_class.get_deduplication_options).to include({ if_deduplicated: :reschedule_once })
+ end
+
context 'when pipeline workers from a stage are running' do
before do
pipeline_tracker.enqueue!
end
it 'does not enqueue the pipeline workers from the next stage and re-enqueues itself' do
- expect_next_instance_of(Gitlab::Import::Logger) do |logger|
+ expect_next_instance_of(BulkImports::Logger) do |logger|
expect(logger).to receive(:info).with(hash_including('message' => 'Stage running', 'entity_stage' => 0))
end
@@ -72,7 +76,7 @@ RSpec.describe BulkImports::EntityWorker, feature_category: :importers do
end
it 'enqueues the pipeline workers from the next stage and re-enqueues itself' do
- expect_next_instance_of(Gitlab::Import::Logger) do |logger|
+ expect_next_instance_of(BulkImports::Logger) do |logger|
expect(logger).to receive(:info).with(hash_including('message' => 'Stage starting', 'entity_stage' => 1))
end
@@ -114,29 +118,25 @@ RSpec.describe BulkImports::EntityWorker, feature_category: :importers do
end
end
- it 'logs and tracks the raised exceptions' do
- exception = StandardError.new('Error!')
-
- expect(BulkImports::PipelineWorker)
- .to receive(:perform_async)
- .and_raise(exception)
-
- expect(Gitlab::ErrorTracking)
- .to receive(:track_exception)
- .with(
- exception,
- hash_including(
- bulk_import_entity_id: entity.id,
- bulk_import_id: entity.bulk_import_id,
- bulk_import_entity_type: entity.source_type,
- source_full_path: entity.source_full_path,
- source_version: entity.bulk_import.source_version_info.to_s,
- importer: 'gitlab_migration'
- )
- )
-
- worker.perform(entity.id)
-
- expect(entity.reload.failed?).to eq(true)
+ describe '#sidekiq_retries_exhausted' do
+ it 'logs export failure and marks entity as failed' do
+ exception = StandardError.new('Exhausted error!')
+
+ expect(Gitlab::ErrorTracking)
+ .to receive(:track_exception)
+ .with(exception, a_hash_including(
+ message: "Request to export #{entity.source_type} failed",
+ bulk_import_entity_id: entity.id,
+ bulk_import_id: entity.bulk_import_id,
+ bulk_import_entity_type: entity.source_type,
+ source_full_path: entity.source_full_path,
+ source_version: entity.bulk_import.source_version_info.to_s,
+ importer: 'gitlab_migration'
+ ))
+
+ described_class.sidekiq_retries_exhausted_block.call({ 'args' => [entity.id] }, exception)
+
+ expect(entity.reload.failed?).to eq(true)
+ end
end
end
diff --git a/spec/workers/bulk_imports/export_request_worker_spec.rb b/spec/workers/bulk_imports/export_request_worker_spec.rb
index 0acc44c5cbf..e9d0b6b24b2 100644
--- a/spec/workers/bulk_imports/export_request_worker_spec.rb
+++ b/spec/workers/bulk_imports/export_request_worker_spec.rb
@@ -71,7 +71,7 @@ RSpec.describe BulkImports::ExportRequestWorker, feature_category: :importers do
entity.update!(source_xid: nil)
- expect_next_instance_of(Gitlab::Import::Logger) do |logger|
+ expect_next_instance_of(BulkImports::Logger) do |logger|
expect(logger).to receive(:error).with(
a_hash_including(
'bulk_import_entity_id' => entity.id,
@@ -82,7 +82,6 @@ RSpec.describe BulkImports::ExportRequestWorker, feature_category: :importers do
'exception.class' => 'NoMethodError',
'exception.message' => /^undefined method `model_id' for nil:NilClass/,
'message' => 'Failed to fetch source entity id',
- 'importer' => 'gitlab_migration',
'source_version' => entity.bulk_import.source_version_info.to_s
)
).twice
diff --git a/spec/workers/bulk_imports/finish_batched_pipeline_worker_spec.rb b/spec/workers/bulk_imports/finish_batched_pipeline_worker_spec.rb
index 5beb11c64aa..59ae4205c0f 100644
--- a/spec/workers/bulk_imports/finish_batched_pipeline_worker_spec.rb
+++ b/spec/workers/bulk_imports/finish_batched_pipeline_worker_spec.rb
@@ -5,40 +5,67 @@ require 'spec_helper'
RSpec.describe BulkImports::FinishBatchedPipelineWorker, feature_category: :importers do
let_it_be(:bulk_import) { create(:bulk_import) }
let_it_be(:config) { create(:bulk_import_configuration, bulk_import: bulk_import) }
- let_it_be(:entity) { create(:bulk_import_entity, bulk_import: bulk_import) }
+ let_it_be(:project) { create(:project, :repository) }
+ let_it_be(:entity) do
+ create(
+ :bulk_import_entity,
+ :project_entity,
+ project: project,
+ bulk_import: bulk_import
+ )
+ end
- let(:status_event) { :finish }
- let(:pipeline_tracker) { create(:bulk_import_tracker, :started, :batched, entity: entity) }
+ let(:pipeline_class) do
+ Class.new do
+ def initialize(_); end
+
+ def on_finish; end
+ end
+ end
+
+ let(:pipeline_tracker) do
+ create(
+ :bulk_import_tracker,
+ :started,
+ :batched,
+ entity: entity,
+ pipeline_name: 'FakePipeline'
+ )
+ end
subject(:worker) { described_class.new }
describe '#perform' do
- context 'when job version is nil' do
- before do
- allow(subject).to receive(:job_version).and_return(nil)
- end
+ before do
+ stub_const('FakePipeline', pipeline_class)
- it 'finishes pipeline and enqueues entity worker' do
- expect(BulkImports::EntityWorker).to receive(:perform_async)
- .with(entity.id)
+ allow_next_instance_of(BulkImports::Projects::Stage) do |instance|
+ allow(instance).to receive(:pipelines)
+ .and_return([{ stage: 0, pipeline: pipeline_class }])
+ end
+ end
- subject.perform(pipeline_tracker.id)
+ context 'when import is in progress' do
+ it 'marks the tracker as finished' do
+ expect_next_instance_of(BulkImports::Logger) do |logger|
+ expect(logger).to receive(:info).with(
+ a_hash_including('message' => 'Tracker finished')
+ )
+ end
- expect(pipeline_tracker.reload.finished?).to eq(true)
+ expect { subject.perform(pipeline_tracker.id) }
+ .to change { pipeline_tracker.reload.finished? }
+ .from(false).to(true)
end
- end
- context 'when job version is present' do
- it 'finishes pipeline and does not enqueues entity worker' do
- expect(BulkImports::EntityWorker).not_to receive(:perform_async)
+ it "calls the pipeline's `#on_finish`" do
+ expect_next_instance_of(pipeline_class) do |pipeline|
+ expect(pipeline).to receive(:on_finish)
+ end
subject.perform(pipeline_tracker.id)
-
- expect(pipeline_tracker.reload.finished?).to eq(true)
end
- end
- context 'when import is in progress' do
it 're-enqueues for any started batches' do
create(:bulk_import_batch_tracker, :started, tracker: pipeline_tracker)
@@ -66,35 +93,51 @@ RSpec.describe BulkImports::FinishBatchedPipelineWorker, feature_category: :impo
it 'fails pipeline tracker and its batches' do
create(:bulk_import_batch_tracker, :finished, tracker: pipeline_tracker)
+ expect_next_instance_of(BulkImports::Logger) do |logger|
+ expect(logger).to receive(:error).with(
+ a_hash_including('message' => 'Tracker stale. Failing batches and tracker')
+ )
+ end
+
subject.perform(pipeline_tracker.id)
expect(pipeline_tracker.reload.failed?).to eq(true)
expect(pipeline_tracker.batches.first.reload.failed?).to eq(true)
end
end
+ end
- context 'when pipeline is not batched' do
- let(:pipeline_tracker) { create(:bulk_import_tracker, :started, entity: entity) }
+ shared_examples 'does nothing' do
+ it "does not call the tracker's `#finish!`" do
+ expect_next_found_instance_of(BulkImports::Tracker) do |instance|
+ expect(instance).not_to receive(:finish!)
+ end
- it 'returns' do
- expect_next_instance_of(BulkImports::Tracker) do |instance|
- expect(instance).not_to receive(:finish!)
- end
+ subject.perform(pipeline_tracker.id)
+ end
- subject.perform(pipeline_tracker.id)
- end
+ it "does not call the pipeline's `#on_finish`" do
+ expect(pipeline_class).not_to receive(:new)
+
+ subject.perform(pipeline_tracker.id)
end
+ end
- context 'when pipeline is not started' do
- let(:status_event) { :start }
+ context 'when tracker is not batched' do
+ let(:pipeline_tracker) { create(:bulk_import_tracker, :started, entity: entity, batched: false) }
- it 'returns' do
- expect_next_instance_of(BulkImports::Tracker) do |instance|
- expect(instance).not_to receive(:finish!)
- end
+ include_examples 'does nothing'
+ end
- described_class.new.perform(pipeline_tracker.id)
- end
- end
+ context 'when tracker is not started' do
+ let(:pipeline_tracker) { create(:bulk_import_tracker, :batched, :finished, entity: entity) }
+
+ include_examples 'does nothing'
+ end
+
+ context 'when pipeline is enqueued' do
+ let(:pipeline_tracker) { create(:bulk_import_tracker, status: 3, entity: entity) }
+
+ include_examples 'does nothing'
end
end
diff --git a/spec/workers/bulk_imports/pipeline_batch_worker_spec.rb b/spec/workers/bulk_imports/pipeline_batch_worker_spec.rb
index 78ce52c41b4..c459c17b1bc 100644
--- a/spec/workers/bulk_imports/pipeline_batch_worker_spec.rb
+++ b/spec/workers/bulk_imports/pipeline_batch_worker_spec.rb
@@ -9,9 +9,17 @@ RSpec.describe BulkImports::PipelineBatchWorker, feature_category: :importers do
let(:pipeline_class) do
Class.new do
- def initialize(_); end
+ def initialize(context)
+ @context = context
+ end
+
+ def self.relation
+ 'labels'
+ end
- def run; end
+ def run
+ @context.tracker.finish!
+ end
def self.file_extraction_pipeline?
false
@@ -35,7 +43,6 @@ RSpec.describe BulkImports::PipelineBatchWorker, feature_category: :importers do
before do
stub_const('FakePipeline', pipeline_class)
- allow(subject).to receive(:jid).and_return('jid')
allow(entity).to receive(:pipeline_exists?).with('FakePipeline').and_return(true)
allow_next_instance_of(BulkImports::Groups::Stage) do |instance|
allow(instance)
@@ -44,51 +51,94 @@ RSpec.describe BulkImports::PipelineBatchWorker, feature_category: :importers do
end
end
+ include_examples 'an idempotent worker' do
+ let(:job_args) { batch.id }
+ let(:tracker) { create(:bulk_import_tracker, :started, entity: entity, pipeline_name: 'FakePipeline') }
+
+ it 'processes the batch once' do
+ allow_next_instance_of(pipeline_class) do |instance|
+ expect(instance).to receive(:run).once.and_call_original
+ end
+
+ perform_multiple(job_args)
+
+ expect(batch.reload).to be_finished
+ end
+ end
+
describe '#perform' do
it 'runs the given pipeline batch successfully' do
expect(BulkImports::FinishBatchedPipelineWorker).to receive(:perform_async).with(tracker.id)
+ expect_next_instance_of(BulkImports::Logger) do |logger|
+ expect(logger).to receive(:info).with(a_hash_including('message' => 'Batch tracker started'))
+ expect(logger).to receive(:info).with(a_hash_including('message' => 'Batch tracker finished'))
+ end
- subject.perform(batch.id)
+ worker.perform(batch.id)
expect(batch.reload).to be_finished
end
- context 'when tracker is failed' do
- let(:tracker) { create(:bulk_import_tracker, :failed) }
+ context 'with tracker status' do
+ context 'when tracker is failed' do
+ let(:tracker) { create(:bulk_import_tracker, :failed) }
- it 'skips the batch' do
- subject.perform(batch.id)
+ it 'skips the batch' do
+ worker.perform(batch.id)
- expect(batch.reload).to be_skipped
+ expect(batch.reload).to be_skipped
+ end
end
- end
- context 'when tracker is finished' do
- let(:tracker) { create(:bulk_import_tracker, :finished) }
+ context 'when tracker is finished' do
+ let(:tracker) { create(:bulk_import_tracker, :finished) }
- it 'skips the batch' do
- subject.perform(batch.id)
+ it 'skips the batch' do
+ worker.perform(batch.id)
- expect(batch.reload).to be_skipped
+ expect(batch.reload).to be_skipped
+ end
end
end
- context 'when batch status is started' do
- let(:batch) { create(:bulk_import_batch_tracker, :started, tracker: tracker) }
+ context 'with batch status' do
+ context 'when batch status is started' do
+ let(:batch) { create(:bulk_import_batch_tracker, :started, tracker: tracker) }
+
+ it 'finishes the batch' do
+ worker.perform(batch.id)
+
+ expect(batch.reload).to be_finished
+ end
+ end
+
+ context 'when batch status is created' do
+ let(:batch) { create(:bulk_import_batch_tracker, :created, tracker: tracker) }
- it 'runs the given pipeline batch successfully' do
- subject.perform(batch.id)
+ it 'finishes the batch' do
+ worker.perform(batch.id)
- expect(batch.reload).to be_finished
+ expect(batch.reload).to be_finished
+ end
+ end
+
+ context 'when batch status is finished' do
+ let(:batch) { create(:bulk_import_batch_tracker, :finished, tracker: tracker) }
+
+ it 'stays finished' do
+ worker.perform(batch.id)
+
+ expect(batch.reload).to be_finished
+ end
end
end
context 'when exclusive lease cannot be obtained' do
it 'does not run the pipeline' do
- expect(subject).to receive(:try_obtain_lease).and_return(false)
- expect(subject).not_to receive(:run)
+ expect(worker).to receive(:try_obtain_lease).and_return(false)
+ expect(worker).not_to receive(:run)
- subject.perform(batch.id)
+ worker.perform(batch.id)
end
end
@@ -104,44 +154,107 @@ RSpec.describe BulkImports::PipelineBatchWorker, feature_category: :importers do
expect(described_class).to receive(:perform_in).with(60, batch.id)
expect(BulkImports::FinishBatchedPipelineWorker).not_to receive(:perform_async).with(tracker.id)
- subject.perform(batch.id)
+ worker.perform(batch.id)
expect(batch.reload).to be_created
end
end
- context 'when pipeline is not retryable' do
- it 'fails the batch and creates a failure record' do
+ context 'when pipeline raises an error' do
+ it 'keeps batch status as `started` and lets the error bubble up' do
allow_next_instance_of(pipeline_class) do |instance|
allow(instance).to receive(:run).and_raise(StandardError, 'Something went wrong')
end
- expect(Gitlab::ErrorTracking).to receive(:track_exception).with(
- instance_of(StandardError),
- hash_including(
- batch_id: batch.id,
- tracker_id: tracker.id,
- pipeline_class: 'FakePipeline',
- pipeline_step: 'pipeline_batch_worker_run'
- )
- )
-
- expect(BulkImports::Failure).to receive(:create).with(
- bulk_import_entity_id: entity.id,
- pipeline_class: 'FakePipeline',
- pipeline_step: 'pipeline_batch_worker_run',
- exception_class: 'StandardError',
- exception_message: 'Something went wrong',
- correlation_id_value: anything
- )
-
- expect(BulkImports::FinishBatchedPipelineWorker).to receive(:perform_async).with(tracker.id)
-
- subject.perform(batch.id)
-
- expect(batch.reload).to be_failed
+ expect { worker.perform(batch.id) }.to raise_exception(StandardError)
+
+ expect(batch.reload).to be_started
end
end
end
end
+
+ describe '.sidekiq_retries_exhausted' do
+ it 'sets batch status to failed' do
+ job = { 'args' => [batch.id] }
+
+ expect(Gitlab::ErrorTracking).to receive(:track_exception).with(
+ instance_of(StandardError),
+ hash_including(
+ 'message' => 'Batch tracker failed',
+ 'batch_id' => batch.id,
+ 'tracker_id' => tracker.id,
+ 'pipeline_class' => 'FakePipeline',
+ 'pipeline_step' => 'pipeline_batch_worker_run',
+ 'importer' => 'gitlab_migration'
+ )
+ )
+
+ expect(BulkImports::Failure).to receive(:create).with(
+ bulk_import_entity_id: entity.id,
+ pipeline_class: 'FakePipeline',
+ pipeline_step: 'pipeline_batch_worker_run',
+ exception_class: 'StandardError',
+ exception_message: 'Something went wrong',
+ correlation_id_value: anything
+ )
+
+ expect(BulkImports::FinishBatchedPipelineWorker).to receive(:perform_async).with(tracker.id)
+
+ described_class.sidekiq_retries_exhausted_block.call(job, StandardError.new("Something went wrong"))
+
+ expect(batch.reload).to be_failed
+ end
+ end
+
+ context 'with stop signal from database health check' do
+ around do |example|
+ with_sidekiq_server_middleware do |chain|
+ chain.add Gitlab::SidekiqMiddleware::SkipJobs
+ Sidekiq::Testing.inline! { example.run }
+ end
+ end
+
+ before do
+ stub_feature_flags("drop_sidekiq_jobs_#{described_class.name}": false)
+
+ stop_signal = instance_double("Gitlab::Database::HealthStatus::Signals::Stop", stop?: true)
+ allow(Gitlab::Database::HealthStatus).to receive(:evaluate).and_return([stop_signal])
+ end
+
+ it 'defers the job by set time' do
+ expect_next_instance_of(described_class) do |worker|
+ expect(worker).not_to receive(:perform).with(batch.id)
+ end
+
+ expect(described_class).to receive(:perform_in).with(described_class::DEFER_ON_HEALTH_DELAY, batch.id)
+
+ described_class.perform_async(batch.id)
+ end
+
+ it 'lazy evaluates schema and tables', :aggregate_failures do
+ block = described_class.database_health_check_attrs[:block]
+
+ job_args = [batch.id]
+
+ schema, table = block.call([job_args])
+
+ expect(schema).to eq(:gitlab_main_cell)
+ expect(table).to eq(['labels'])
+ end
+
+ context 'when `bulk_import_deferred_workers` feature flag is disabled' do
+ it 'does not defer job execution' do
+ stub_feature_flags(bulk_import_deferred_workers: false)
+
+ expect_next_instance_of(described_class) do |worker|
+ expect(worker).to receive(:perform).with(batch.id)
+ end
+
+ expect(described_class).not_to receive(:perform_in)
+
+ described_class.perform_async(batch.id)
+ end
+ end
+ end
end
diff --git a/spec/workers/bulk_imports/pipeline_worker_spec.rb b/spec/workers/bulk_imports/pipeline_worker_spec.rb
index e1259d5666d..d99b3e9de73 100644
--- a/spec/workers/bulk_imports/pipeline_worker_spec.rb
+++ b/spec/workers/bulk_imports/pipeline_worker_spec.rb
@@ -9,6 +9,10 @@ RSpec.describe BulkImports::PipelineWorker, feature_category: :importers do
def run; end
+ def self.relation
+ 'labels'
+ end
+
def self.file_extraction_pipeline?
false
end
@@ -28,6 +32,8 @@ RSpec.describe BulkImports::PipelineWorker, feature_category: :importers do
)
end
+ let(:worker) { described_class.new }
+
before do
stub_const('FakePipeline', pipeline_class)
@@ -38,13 +44,28 @@ RSpec.describe BulkImports::PipelineWorker, feature_category: :importers do
end
end
+ include_examples 'an idempotent worker' do
+ let(:job_args) { [pipeline_tracker.id, pipeline_tracker.stage, entity.id] }
+
+ it 'runs the pipeline and sets tracker to finished' do
+ allow(worker).to receive(:jid).and_return('jid')
+
+ perform_multiple(job_args, worker: worker)
+
+ pipeline_tracker.reload
+
+ expect(pipeline_tracker.status_name).to eq(:finished)
+ expect(pipeline_tracker.jid).to eq('jid')
+ end
+ end
+
it 'runs the given pipeline successfully' do
- expect_next_instance_of(Gitlab::Import::Logger) do |logger|
+ expect_next_instance_of(BulkImports::Logger) do |logger|
expect(logger)
.to receive(:info)
.with(
hash_including(
- 'pipeline_name' => 'FakePipeline',
+ 'pipeline_class' => 'FakePipeline',
'bulk_import_id' => entity.bulk_import_id,
'bulk_import_entity_id' => entity.id,
'bulk_import_entity_type' => entity.source_type,
@@ -53,9 +74,9 @@ RSpec.describe BulkImports::PipelineWorker, feature_category: :importers do
)
end
- allow(subject).to receive(:jid).and_return('jid')
+ allow(worker).to receive(:jid).and_return('jid')
- subject.perform(pipeline_tracker.id, pipeline_tracker.stage, entity.id)
+ worker.perform(pipeline_tracker.id, pipeline_tracker.stage, entity.id)
pipeline_tracker.reload
@@ -63,74 +84,30 @@ RSpec.describe BulkImports::PipelineWorker, feature_category: :importers do
expect(pipeline_tracker.jid).to eq('jid')
end
- context 'when job version is nil' do
- before do
- allow(subject).to receive(:job_version).and_return(nil)
- end
-
- it 'runs the given pipeline successfully and enqueues entity worker' do
- expect(BulkImports::EntityWorker).to receive(:perform_async).with(entity.id)
-
- subject.perform(pipeline_tracker.id, pipeline_tracker.stage, entity.id)
-
- pipeline_tracker.reload
-
- expect(pipeline_tracker.status_name).to eq(:finished)
- end
-
- context 'when an error occurs' do
- it 'enqueues entity worker' do
- expect_next_instance_of(pipeline_class) do |pipeline|
- expect(pipeline)
- .to receive(:run)
- .and_raise(StandardError, 'Error!')
- end
-
- expect(BulkImports::EntityWorker).to receive(:perform_async).with(entity.id)
-
- subject.perform(pipeline_tracker.id, pipeline_tracker.stage, entity.id)
- end
- end
- end
-
context 'when exclusive lease cannot be obtained' do
it 'does not run the pipeline' do
- expect(subject).to receive(:try_obtain_lease).and_return(false)
- expect(subject).not_to receive(:run)
+ expect(worker).to receive(:try_obtain_lease).and_return(false)
+ expect(worker).not_to receive(:run)
- subject.perform(pipeline_tracker.id, pipeline_tracker.stage, entity.id)
+ worker.perform(pipeline_tracker.id, pipeline_tracker.stage, entity.id)
end
end
- context 'when the pipeline raises an exception' do
- it 'logs the error' do
- pipeline_tracker = create(
- :bulk_import_tracker,
- entity: entity,
- pipeline_name: 'FakePipeline',
- status_event: 'enqueue'
- )
-
- allow(subject).to receive(:jid).and_return('jid')
-
- expect_next_instance_of(pipeline_class) do |pipeline|
- expect(pipeline)
- .to receive(:run)
- .and_raise(StandardError, 'Error!')
- end
+ describe '.sidekiq_retries_exhausted' do
+ it 'logs and sets status as failed' do
+ job = { 'args' => [pipeline_tracker.id, pipeline_tracker.stage, entity.id] }
- expect_next_instance_of(Gitlab::Import::Logger) do |logger|
+ expect_next_instance_of(BulkImports::Logger) do |logger|
expect(logger)
.to receive(:error)
.with(
hash_including(
- 'pipeline_name' => 'FakePipeline',
+ 'pipeline_class' => 'FakePipeline',
'bulk_import_entity_id' => entity.id,
'bulk_import_id' => entity.bulk_import_id,
'bulk_import_entity_type' => entity.source_type,
'source_full_path' => entity.source_full_path,
'class' => 'BulkImports::PipelineWorker',
- 'exception.backtrace' => anything,
'exception.message' => 'Error!',
'message' => 'Pipeline failed',
'source_version' => entity.bulk_import.source_version_info.to_s,
@@ -148,7 +125,7 @@ RSpec.describe BulkImports::PipelineWorker, feature_category: :importers do
'bulk_import_id' => entity.bulk_import.id,
'bulk_import_entity_type' => entity.source_type,
'source_full_path' => entity.source_full_path,
- 'pipeline_name' => pipeline_tracker.pipeline_name,
+ 'pipeline_class' => pipeline_tracker.pipeline_name,
'importer' => 'gitlab_migration',
'source_version' => entity.bulk_import.source_version_info.to_s
)
@@ -167,84 +144,127 @@ RSpec.describe BulkImports::PipelineWorker, feature_category: :importers do
)
)
- subject.perform(pipeline_tracker.id, pipeline_tracker.stage, entity.id)
+ expect_next_instance_of(described_class) do |worker|
+ expect(worker).to receive(:perform_failure).with(pipeline_tracker.id, entity.id, StandardError)
+ .and_call_original
+ allow(worker).to receive(:jid).and_return('jid')
+ end
+
+ described_class.sidekiq_retries_exhausted_block.call(job, StandardError.new('Error!'))
pipeline_tracker.reload
expect(pipeline_tracker.status_name).to eq(:failed)
expect(pipeline_tracker.jid).to eq('jid')
end
+ end
- context 'when enqueued pipeline cannot be found' do
- shared_examples 'logs the error' do
- it 'logs the error' do
- expect_next_instance_of(Gitlab::Import::Logger) do |logger|
- status = pipeline_tracker.human_status_name
-
- expect(logger)
- .to receive(:error)
- .with(
- hash_including(
- 'bulk_import_entity_id' => entity.id,
- 'bulk_import_id' => entity.bulk_import_id,
- 'bulk_import_entity_type' => entity.source_type,
- 'pipeline_tracker_id' => pipeline_tracker.id,
- 'pipeline_tracker_state' => status,
- 'pipeline_name' => pipeline_tracker.pipeline_name,
- 'source_full_path' => entity.source_full_path,
- 'source_version' => entity.bulk_import.source_version_info.to_s,
- 'importer' => 'gitlab_migration',
- 'message' => "Pipeline in #{status} state instead of expected enqueued state"
- )
- )
- end
-
- subject.perform(pipeline_tracker.id, pipeline_tracker.stage, entity.id)
- end
+ context 'with stop signal from database health check' do
+ around do |example|
+ with_sidekiq_server_middleware do |chain|
+ chain.add Gitlab::SidekiqMiddleware::SkipJobs
+ Sidekiq::Testing.inline! { example.run }
end
+ end
- context 'when pipeline is finished' do
- let(:pipeline_tracker) do
- create(
- :bulk_import_tracker,
- :finished,
- entity: entity,
- pipeline_name: 'FakePipeline'
- )
- end
+ before do
+ stub_feature_flags("drop_sidekiq_jobs_#{described_class.name}": false)
- include_examples 'logs the error'
+ stop_signal = instance_double("Gitlab::Database::HealthStatus::Signals::Stop", stop?: true)
+ allow(Gitlab::Database::HealthStatus).to receive(:evaluate).and_return([stop_signal])
+ end
+
+ it 'defers the job by set time' do
+ expect_next_instance_of(described_class) do |worker|
+ expect(worker).not_to receive(:perform).with(pipeline_tracker.id, pipeline_tracker.stage, entity.id)
end
- context 'when pipeline is skipped' do
- let(:pipeline_tracker) do
- create(
- :bulk_import_tracker,
- :skipped,
- entity: entity,
- pipeline_name: 'FakePipeline'
- )
- end
+ expect(described_class).to receive(:perform_in).with(
+ described_class::DEFER_ON_HEALTH_DELAY,
+ pipeline_tracker.id,
+ pipeline_tracker.stage,
+ entity.id
+ )
- include_examples 'logs the error'
- end
+ described_class.perform_async(pipeline_tracker.id, pipeline_tracker.stage, entity.id)
+ end
- context 'when tracker is started' do
- it 'marks tracker as failed' do
- pipeline_tracker = create(
- :bulk_import_tracker,
- :started,
- entity: entity,
- pipeline_name: 'FakePipeline'
- )
+ it 'lazy evaluates schema and tables', :aggregate_failures do
+ block = described_class.database_health_check_attrs[:block]
+
+ job_args = [pipeline_tracker.id, pipeline_tracker.stage, entity.id]
+
+ schema, table = block.call([job_args])
+
+ expect(schema).to eq(:gitlab_main_cell)
+ expect(table).to eq(['labels'])
+ end
- subject.perform(pipeline_tracker.id, pipeline_tracker.stage, entity.id)
+ context 'when `bulk_import_deferred_workers` feature flag is disabled' do
+ it 'does not defer job execution' do
+ stub_feature_flags(bulk_import_deferred_workers: false)
- expect(pipeline_tracker.reload.failed?).to eq(true)
+ expect_next_instance_of(described_class) do |worker|
+ expect(worker).to receive(:perform).with(pipeline_tracker.id, pipeline_tracker.stage, entity.id)
end
+
+ expect(described_class).not_to receive(:perform_in)
+
+ described_class.perform_async(pipeline_tracker.id, pipeline_tracker.stage, entity.id)
end
end
+ end
+
+ context 'when pipeline is finished' do
+ let(:pipeline_tracker) do
+ create(
+ :bulk_import_tracker,
+ :finished,
+ entity: entity,
+ pipeline_name: 'FakePipeline'
+ )
+ end
+ it 'no-ops and returns' do
+ expect(described_class).not_to receive(:run)
+
+ worker.perform(pipeline_tracker.id, pipeline_tracker.stage, entity.id)
+ end
+ end
+
+ context 'when pipeline is skipped' do
+ let(:pipeline_tracker) do
+ create(
+ :bulk_import_tracker,
+ :skipped,
+ entity: entity,
+ pipeline_name: 'FakePipeline'
+ )
+ end
+
+ it 'no-ops and returns' do
+ expect(described_class).not_to receive(:run)
+
+ worker.perform(pipeline_tracker.id, pipeline_tracker.stage, entity.id)
+ end
+ end
+
+ context 'when tracker is started' do
+ it 'runs the pipeline' do
+ pipeline_tracker = create(
+ :bulk_import_tracker,
+ :started,
+ entity: entity,
+ pipeline_name: 'FakePipeline'
+ )
+
+ worker.perform(pipeline_tracker.id, pipeline_tracker.stage, entity.id)
+
+ expect(pipeline_tracker.reload.finished?).to eq(true)
+ end
+ end
+
+ describe '#perform' do
context 'when entity is failed' do
it 'marks tracker as skipped and logs the skip' do
pipeline_tracker = create(
@@ -256,14 +276,14 @@ RSpec.describe BulkImports::PipelineWorker, feature_category: :importers do
entity.update!(status: -1)
- expect_next_instance_of(Gitlab::Import::Logger) do |logger|
+ expect_next_instance_of(BulkImports::Logger) do |logger|
allow(logger).to receive(:info)
expect(logger)
.to receive(:info)
.with(
hash_including(
- 'pipeline_name' => 'FakePipeline',
+ 'pipeline_class' => 'FakePipeline',
'bulk_import_entity_id' => entity.id,
'bulk_import_id' => entity.bulk_import_id,
'bulk_import_entity_type' => entity.source_type,
@@ -273,7 +293,7 @@ RSpec.describe BulkImports::PipelineWorker, feature_category: :importers do
)
end
- subject.perform(pipeline_tracker.id, pipeline_tracker.stage, entity.id)
+ worker.perform(pipeline_tracker.id, pipeline_tracker.stage, entity.id)
expect(pipeline_tracker.reload.status_name).to eq(:skipped)
end
@@ -294,7 +314,7 @@ RSpec.describe BulkImports::PipelineWorker, feature_category: :importers do
end
before do
- allow(subject).to receive(:jid).and_return('jid')
+ allow(worker).to receive(:jid).and_return('jid')
expect_next_instance_of(pipeline_class) do |pipeline|
expect(pipeline)
@@ -308,12 +328,12 @@ RSpec.describe BulkImports::PipelineWorker, feature_category: :importers do
expect(tracker).to receive(:retry).and_call_original
end
- expect_next_instance_of(Gitlab::Import::Logger) do |logger|
+ expect_next_instance_of(BulkImports::Logger) do |logger|
expect(logger)
.to receive(:info)
.with(
hash_including(
- 'pipeline_name' => 'FakePipeline',
+ 'pipeline_class' => 'FakePipeline',
'bulk_import_entity_id' => entity.id,
'bulk_import_id' => entity.bulk_import_id,
'bulk_import_entity_type' => entity.source_type,
@@ -331,7 +351,7 @@ RSpec.describe BulkImports::PipelineWorker, feature_category: :importers do
pipeline_tracker.entity.id
)
- subject.perform(pipeline_tracker.id, pipeline_tracker.stage, entity.id)
+ worker.perform(pipeline_tracker.id, pipeline_tracker.stage, entity.id)
pipeline_tracker.reload
@@ -384,7 +404,7 @@ RSpec.describe BulkImports::PipelineWorker, feature_category: :importers do
allow(status).to receive(:batched?).and_return(false)
end
- subject.perform(pipeline_tracker.id, pipeline_tracker.stage, entity.id)
+ worker.perform(pipeline_tracker.id, pipeline_tracker.stage, entity.id)
expect(pipeline_tracker.reload.status_name).to eq(:finished)
end
@@ -407,7 +427,7 @@ RSpec.describe BulkImports::PipelineWorker, feature_category: :importers do
entity.id
)
- subject.perform(pipeline_tracker.id, pipeline_tracker.stage, entity.id)
+ worker.perform(pipeline_tracker.id, pipeline_tracker.stage, entity.id)
end
end
@@ -436,7 +456,7 @@ RSpec.describe BulkImports::PipelineWorker, feature_category: :importers do
entity.id
)
- subject.perform(pipeline_tracker.id, pipeline_tracker.stage, entity.id)
+ worker.perform(pipeline_tracker.id, pipeline_tracker.stage, entity.id)
expect(pipeline_tracker.reload.status_name).to eq(:enqueued)
end
@@ -445,31 +465,9 @@ RSpec.describe BulkImports::PipelineWorker, feature_category: :importers do
context 'when empty export timeout is reached' do
let(:created_at) { 10.minutes.ago }
- it 'marks as failed and logs the error' do
- expect_next_instance_of(Gitlab::Import::Logger) do |logger|
- expect(logger)
- .to receive(:error)
- .with(
- hash_including(
- 'pipeline_name' => 'NdjsonPipeline',
- 'bulk_import_entity_id' => entity.id,
- 'bulk_import_id' => entity.bulk_import_id,
- 'bulk_import_entity_type' => entity.source_type,
- 'source_full_path' => entity.source_full_path,
- 'class' => 'BulkImports::PipelineWorker',
- 'exception.backtrace' => anything,
- 'exception.class' => 'BulkImports::Pipeline::ExpiredError',
- 'exception.message' => 'Empty export status on source instance',
- 'importer' => 'gitlab_migration',
- 'message' => 'Pipeline failed',
- 'source_version' => entity.bulk_import.source_version_info.to_s
- )
- )
- end
-
- subject.perform(pipeline_tracker.id, pipeline_tracker.stage, entity.id)
-
- expect(pipeline_tracker.reload.status_name).to eq(:failed)
+ it 'raises sidekiq error' do
+ expect { worker.perform(pipeline_tracker.id, pipeline_tracker.stage, entity.id) }
+ .to raise_exception(BulkImports::Pipeline::ExpiredError)
end
end
@@ -479,17 +477,8 @@ RSpec.describe BulkImports::PipelineWorker, feature_category: :importers do
it 'falls back to entity created_at' do
entity.update!(created_at: 10.minutes.ago)
- expect_next_instance_of(Gitlab::Import::Logger) do |logger|
- expect(logger)
- .to receive(:error)
- .with(
- hash_including('exception.message' => 'Empty export status on source instance')
- )
- end
-
- subject.perform(pipeline_tracker.id, pipeline_tracker.stage, entity.id)
-
- expect(pipeline_tracker.reload.status_name).to eq(:failed)
+ expect { worker.perform(pipeline_tracker.id, pipeline_tracker.stage, entity.id) }
+ .to raise_exception(BulkImports::Pipeline::ExpiredError)
end
end
end
@@ -501,28 +490,8 @@ RSpec.describe BulkImports::PipelineWorker, feature_category: :importers do
allow(status).to receive(:error).and_return('Error!')
end
- expect_next_instance_of(Gitlab::Import::Logger) do |logger|
- expect(logger)
- .to receive(:error)
- .with(
- hash_including(
- 'pipeline_name' => 'NdjsonPipeline',
- 'bulk_import_entity_id' => entity.id,
- 'bulk_import_id' => entity.bulk_import_id,
- 'bulk_import_entity_type' => entity.source_type,
- 'source_full_path' => entity.source_full_path,
- 'exception.backtrace' => anything,
- 'exception.class' => 'BulkImports::Pipeline::FailedError',
- 'exception.message' => 'Export from source instance failed: Error!',
- 'importer' => 'gitlab_migration',
- 'source_version' => entity.bulk_import.source_version_info.to_s
- )
- )
- end
-
- subject.perform(pipeline_tracker.id, pipeline_tracker.stage, entity.id)
-
- expect(pipeline_tracker.reload.status_name).to eq(:failed)
+ expect { worker.perform(pipeline_tracker.id, pipeline_tracker.stage, entity.id) }
+ .to raise_exception(BulkImports::Pipeline::FailedError)
end
end
@@ -542,7 +511,7 @@ RSpec.describe BulkImports::PipelineWorker, feature_category: :importers do
it 'enqueues pipeline batches' do
expect(BulkImports::PipelineBatchWorker).to receive(:perform_async).twice
- subject.perform(pipeline_tracker.id, pipeline_tracker.stage, entity.id)
+ worker.perform(pipeline_tracker.id, pipeline_tracker.stage, entity.id)
pipeline_tracker.reload
@@ -555,9 +524,9 @@ RSpec.describe BulkImports::PipelineWorker, feature_category: :importers do
let(:batches_count) { 0 }
it 'marks tracker as finished' do
- expect(subject).not_to receive(:enqueue_batches)
+ expect(worker).not_to receive(:enqueue_batches)
- subject.perform(pipeline_tracker.id, pipeline_tracker.stage, entity.id)
+ worker.perform(pipeline_tracker.id, pipeline_tracker.stage, entity.id)
expect(pipeline_tracker.reload.status_name).to eq(:finished)
end
diff --git a/spec/workers/bulk_imports/relation_batch_export_worker_spec.rb b/spec/workers/bulk_imports/relation_batch_export_worker_spec.rb
index 4a2c8d48742..8ee55d64a1b 100644
--- a/spec/workers/bulk_imports/relation_batch_export_worker_spec.rb
+++ b/spec/workers/bulk_imports/relation_batch_export_worker_spec.rb
@@ -15,7 +15,7 @@ RSpec.describe BulkImports::RelationBatchExportWorker, feature_category: :import
expect(BulkImports::RelationBatchExportService)
.to receive(:new)
- .with(user.id, batch.id)
+ .with(user, batch)
.twice.and_return(service)
expect(service).to receive(:execute).twice
@@ -23,4 +23,21 @@ RSpec.describe BulkImports::RelationBatchExportWorker, feature_category: :import
end
end
end
+
+ describe '.sidekiq_retries_exhausted' do
+ let(:job) { { 'args' => job_args } }
+
+ it 'sets export status to failed and tracks the exception' do
+ portable = batch.export.portable
+
+ expect(Gitlab::ErrorTracking)
+ .to receive(:track_exception)
+ .with(kind_of(StandardError), portable_id: portable.id, portable_type: portable.class.name)
+
+ described_class.sidekiq_retries_exhausted_block.call(job, StandardError.new('*' * 300))
+
+ expect(batch.reload.failed?).to eq(true)
+ expect(batch.error.size).to eq(255)
+ end
+ end
end
diff --git a/spec/workers/bulk_imports/relation_export_worker_spec.rb b/spec/workers/bulk_imports/relation_export_worker_spec.rb
index 646af6c2a9c..55e2a238027 100644
--- a/spec/workers/bulk_imports/relation_export_worker_spec.rb
+++ b/spec/workers/bulk_imports/relation_export_worker_spec.rb
@@ -63,4 +63,20 @@ RSpec.describe BulkImports::RelationExportWorker, feature_category: :importers d
end
end
end
+
+ describe '.sidekiq_retries_exhausted' do
+ let(:job) { { 'args' => job_args } }
+ let!(:export) { create(:bulk_import_export, group: group, relation: relation) }
+
+ it 'sets export status to failed and tracks the exception' do
+ expect(Gitlab::ErrorTracking)
+ .to receive(:track_exception)
+ .with(kind_of(StandardError), portable_id: group.id, portable_type: group.class.name)
+
+ described_class.sidekiq_retries_exhausted_block.call(job, StandardError.new('*' * 300))
+
+ expect(export.reload.failed?).to eq(true)
+ expect(export.error.size).to eq(255)
+ end
+ end
end
diff --git a/spec/workers/bulk_imports/stuck_import_worker_spec.rb b/spec/workers/bulk_imports/stuck_import_worker_spec.rb
index ba1b1b66b00..eadf3864190 100644
--- a/spec/workers/bulk_imports/stuck_import_worker_spec.rb
+++ b/spec/workers/bulk_imports/stuck_import_worker_spec.rb
@@ -9,17 +9,46 @@ RSpec.describe BulkImports::StuckImportWorker, feature_category: :importers do
let_it_be(:stale_started_bulk_import) { create(:bulk_import, :started, created_at: 3.days.ago) }
let_it_be(:stale_created_bulk_import_entity) { create(:bulk_import_entity, :created, created_at: 3.days.ago) }
let_it_be(:stale_started_bulk_import_entity) { create(:bulk_import_entity, :started, created_at: 3.days.ago) }
- let_it_be(:started_bulk_import_tracker) { create(:bulk_import_tracker, :started, entity: stale_started_bulk_import_entity) }
+
+ let_it_be(:started_bulk_import_tracker) do
+ create(:bulk_import_tracker, :started, entity: stale_started_bulk_import_entity)
+ end
subject { described_class.new.perform }
describe 'perform' do
it 'updates the status of bulk imports to timeout' do
+ expect_next_instance_of(BulkImports::Logger) do |logger|
+ allow(logger).to receive(:error)
+ expect(logger).to receive(:error).with(
+ message: 'BulkImport stale',
+ bulk_import_id: stale_created_bulk_import.id
+ )
+ expect(logger).to receive(:error).with(
+ message: 'BulkImport stale',
+ bulk_import_id: stale_started_bulk_import.id
+ )
+ end
+
expect { subject }.to change { stale_created_bulk_import.reload.status_name }.from(:created).to(:timeout)
.and change { stale_started_bulk_import.reload.status_name }.from(:started).to(:timeout)
end
it 'updates the status of bulk import entities to timeout' do
+ expect_next_instance_of(BulkImports::Logger) do |logger|
+ allow(logger).to receive(:error)
+ expect(logger).to receive(:error).with(
+ message: 'BulkImports::Entity stale',
+ bulk_import_entity_id: stale_created_bulk_import_entity.id,
+ bulk_import_id: stale_created_bulk_import_entity.bulk_import_id
+ )
+ expect(logger).to receive(:error).with(
+ message: 'BulkImports::Entity stale',
+ bulk_import_entity_id: stale_started_bulk_import_entity.id,
+ bulk_import_id: stale_started_bulk_import_entity.bulk_import_id
+ )
+ end
+
expect { subject }.to change { stale_created_bulk_import_entity.reload.status_name }.from(:created).to(:timeout)
.and change { stale_started_bulk_import_entity.reload.status_name }.from(:started).to(:timeout)
end
diff --git a/spec/workers/ci/cancel_pipeline_worker_spec.rb b/spec/workers/ci/cancel_pipeline_worker_spec.rb
index 13a9c0affe7..8e8f9a78132 100644
--- a/spec/workers/ci/cancel_pipeline_worker_spec.rb
+++ b/spec/workers/ci/cancel_pipeline_worker_spec.rb
@@ -11,14 +11,13 @@ RSpec.describe Ci::CancelPipelineWorker, :aggregate_failures, feature_category:
let(:cancel_service) { instance_double(::Ci::CancelPipelineService) }
it 'cancels the pipeline' do
- allow(::Ci::Pipeline).to receive(:find_by_id).and_return(pipeline)
+ allow(::Ci::Pipeline).to receive(:find_by_id).twice.and_return(pipeline)
expect(::Ci::CancelPipelineService)
.to receive(:new)
.with(
pipeline: pipeline,
current_user: nil,
- auto_canceled_by_pipeline_id:
- pipeline.id,
+ auto_canceled_by_pipeline: pipeline,
cascade_to_children: false)
.and_return(cancel_service)
@@ -28,7 +27,7 @@ RSpec.describe Ci::CancelPipelineWorker, :aggregate_failures, feature_category:
end
context 'if pipeline is deleted' do
- subject(:perform) { described_class.new.perform(non_existing_record_id, non_existing_record_id) }
+ subject(:perform) { described_class.new.perform(non_existing_record_id, pipeline.id) }
it 'does not error' do
expect(::Ci::CancelPipelineService).not_to receive(:new)
@@ -37,6 +36,23 @@ RSpec.describe Ci::CancelPipelineWorker, :aggregate_failures, feature_category:
end
end
+ context 'when auto_canceled_by_pipeline is deleted' do
+ subject(:perform) { described_class.new.perform(pipeline.id, non_existing_record_id) }
+
+ it 'does not error' do
+ expect(::Ci::CancelPipelineService)
+ .to receive(:new)
+ .with(
+ pipeline: an_instance_of(::Ci::Pipeline),
+ current_user: nil,
+ auto_canceled_by_pipeline: nil,
+ cascade_to_children: false)
+ .and_call_original
+
+ perform
+ end
+ end
+
describe 'with builds and state transition side effects', :sidekiq_inline do
let!(:build) { create(:ci_build, :running, pipeline: pipeline) }
diff --git a/spec/workers/ci/initial_pipeline_process_worker_spec.rb b/spec/workers/ci/initial_pipeline_process_worker_spec.rb
index 9a94f1cbb4c..fcdd0a2a33b 100644
--- a/spec/workers/ci/initial_pipeline_process_worker_spec.rb
+++ b/spec/workers/ci/initial_pipeline_process_worker_spec.rb
@@ -70,32 +70,6 @@ RSpec.describe Ci::InitialPipelineProcessWorker, feature_category: :continuous_i
subject
end
-
- context 'when `create_deployment_only_for_processable_jobs` FF is disabled' do
- before do
- stub_feature_flags(create_deployment_only_for_processable_jobs: false)
- end
-
- it 'creates a deployment record' do
- expect { subject }.to change { Deployment.count }.by(1)
-
- expect(job.deployment).to have_attributes(
- project: job.project,
- ref: job.ref,
- sha: job.sha,
- deployable: job,
- deployable_type: 'CommitStatus',
- environment: job.persisted_environment
- )
- end
-
- it 'a deployment is created before atomic processing is kicked off' do
- expect(::Deployments::CreateForJobService).to receive(:new).ordered
- expect(::Ci::PipelineProcessing::AtomicProcessingService).to receive(:new).ordered
-
- subject
- end
- end
end
end
end
diff --git a/spec/workers/ci/pipeline_success_unlock_artifacts_worker_spec.rb b/spec/workers/ci/pipeline_success_unlock_artifacts_worker_spec.rb
index 60a34fdab53..d5f3c2b92fb 100644
--- a/spec/workers/ci/pipeline_success_unlock_artifacts_worker_spec.rb
+++ b/spec/workers/ci/pipeline_success_unlock_artifacts_worker_spec.rb
@@ -75,7 +75,8 @@ RSpec.describe Ci::PipelineSuccessUnlockArtifactsWorker, feature_category: :buil
expect(described_class.database_health_check_attrs).to eq(
gitlab_schema: :gitlab_ci,
delay_by: described_class::DEFAULT_DEFER_DELAY,
- tables: [:ci_job_artifacts]
+ tables: [:ci_job_artifacts],
+ block: nil
)
end
end
diff --git a/spec/workers/ci/refs/unlock_previous_pipelines_worker_spec.rb b/spec/workers/ci/refs/unlock_previous_pipelines_worker_spec.rb
index 2f00ea45edc..558ac5b9e0b 100644
--- a/spec/workers/ci/refs/unlock_previous_pipelines_worker_spec.rb
+++ b/spec/workers/ci/refs/unlock_previous_pipelines_worker_spec.rb
@@ -17,6 +17,7 @@ RSpec.describe Ci::Refs::UnlockPreviousPipelinesWorker, :unlock_pipelines, :clea
create(
:ci_pipeline,
:with_persisted_artifacts,
+ :artifacts_locked,
ref: older_pipeline.ref,
tag: older_pipeline.tag,
project: older_pipeline.project
@@ -25,12 +26,30 @@ RSpec.describe Ci::Refs::UnlockPreviousPipelinesWorker, :unlock_pipelines, :clea
describe '#perform' do
it 'executes a service' do
+ ci_ref = pipeline.ci_ref
+ expect(ci_ref).to receive(:last_unlockable_ci_source_pipeline).and_return(pipeline)
+
+ expect(Ci::Ref).to receive(:find_by_id).with(pipeline.ci_ref.id).and_return(ci_ref)
+
expect_next_instance_of(Ci::Refs::EnqueuePipelinesToUnlockService) do |instance|
- expect(instance).to receive(:execute).and_call_original
+ expect(instance).to receive(:execute).with(ci_ref, before_pipeline: pipeline).and_call_original
end
worker.perform(pipeline.ci_ref.id)
end
+
+ context 'when ref has no pipelines locked' do
+ before do
+ older_pipeline.update!(locked: :unlocked)
+ pipeline.update!(locked: :unlocked)
+ end
+
+ it 'does nothing' do
+ expect(Ci::Refs::EnqueuePipelinesToUnlockService).not_to receive(:new)
+
+ worker.perform(pipeline.ci_ref.id)
+ end
+ end
end
it_behaves_like 'an idempotent worker' do
diff --git a/spec/workers/concerns/gitlab/github_import/stage_methods_spec.rb b/spec/workers/concerns/gitlab/github_import/stage_methods_spec.rb
index c8f7427d5ae..fa782967441 100644
--- a/spec/workers/concerns/gitlab/github_import/stage_methods_spec.rb
+++ b/spec/workers/concerns/gitlab/github_import/stage_methods_spec.rb
@@ -145,12 +145,16 @@ RSpec.describe Gitlab::GithubImport::StageMethods, feature_category: :importers
.to receive(:import)
.with(client, project)
+ expect(project.import_state).to receive(:refresh_jid_expiration)
+
worker.try_import(client, project)
end
it 'reschedules the worker if RateLimitError was raised' do
client = double(:client, rate_limit_resets_in: 10)
+ expect(project.import_state).to receive(:refresh_jid_expiration)
+
expect(worker)
.to receive(:import)
.with(client, project)
@@ -181,4 +185,30 @@ RSpec.describe Gitlab::GithubImport::StageMethods, feature_category: :importers
expect(worker.find_project(-1)).to be_nil
end
end
+
+ describe '.resumes_work_when_interrupted!' do
+ subject(:sidekiq_options) { worker.class.sidekiq_options }
+
+ it 'does not set the `max_retries_after_interruption` if not called' do
+ is_expected.not_to have_key('max_retries_after_interruption')
+ end
+
+ it 'sets the `max_retries_after_interruption`' do
+ worker.class.resumes_work_when_interrupted!
+
+ is_expected.to include('max_retries_after_interruption' => 20)
+ end
+
+ context 'when the flag is disabled' do
+ before do
+ stub_feature_flags(github_importer_raise_max_interruptions: false)
+ end
+
+ it 'does not set `max_retries_after_interruption`' do
+ worker.class.resumes_work_when_interrupted!
+
+ is_expected.not_to have_key('max_retries_after_interruption')
+ end
+ end
+ end
end
diff --git a/spec/workers/concerns/worker_attributes_spec.rb b/spec/workers/concerns/worker_attributes_spec.rb
index 90c07a9c959..767a55162fb 100644
--- a/spec/workers/concerns/worker_attributes_spec.rb
+++ b/spec/workers/concerns/worker_attributes_spec.rb
@@ -37,7 +37,7 @@ RSpec.describe WorkerAttributes, feature_category: :shared do
:worker_has_external_dependencies? | :worker_has_external_dependencies! | false | [] | true
:idempotent? | :idempotent! | false | [] | true
:big_payload? | :big_payload! | false | [] | true
- :database_health_check_attrs | :defer_on_database_health_signal | nil | [:gitlab_main, [:users], 1.minute] | { gitlab_schema: :gitlab_main, tables: [:users], delay_by: 1.minute }
+ :database_health_check_attrs | :defer_on_database_health_signal | nil | [:gitlab_main, [:users], 1.minute] | { gitlab_schema: :gitlab_main, tables: [:users], delay_by: 1.minute, block: nil }
end
# rubocop: enable Layout/LineLength
diff --git a/spec/workers/concerns/worker_context_spec.rb b/spec/workers/concerns/worker_context_spec.rb
index 700d9e37a55..7a046517fd1 100644
--- a/spec/workers/concerns/worker_context_spec.rb
+++ b/spec/workers/concerns/worker_context_spec.rb
@@ -74,7 +74,7 @@ RSpec.describe WorkerContext, feature_category: :shared do
describe '.bulk_perform_async_with_contexts' do
subject do
worker.bulk_perform_async_with_contexts(
- %w(hello world),
+ %w[hello world],
context_proc: -> (_) { { user: build_stubbed(:user) } },
arguments_proc: -> (word) { word }
)
@@ -93,7 +93,7 @@ RSpec.describe WorkerContext, feature_category: :shared do
subject do
worker.bulk_perform_in_with_contexts(
10.minutes,
- %w(hello world),
+ %w[hello world],
context_proc: -> (_) { { user: build_stubbed(:user) } },
arguments_proc: -> (word) { word }
)
diff --git a/spec/workers/container_registry/migration/enqueuer_worker_spec.rb b/spec/workers/container_registry/migration/enqueuer_worker_spec.rb
index 4a603e538ef..ff388b1a29d 100644
--- a/spec/workers/container_registry/migration/enqueuer_worker_spec.rb
+++ b/spec/workers/container_registry/migration/enqueuer_worker_spec.rb
@@ -16,7 +16,7 @@ RSpec.describe ContainerRegistry::Migration::EnqueuerWorker, :aggregate_failures
before do
stub_container_registry_config(enabled: true)
stub_application_setting(container_registry_import_created_before: 1.day.ago)
- stub_container_registry_tags(repository: container_repository.path, tags: %w(tag1 tag2 tag3), with_manifest: true)
+ stub_container_registry_tags(repository: container_repository.path, tags: %w[tag1 tag2 tag3], with_manifest: true)
end
describe '#perform' do
@@ -133,7 +133,7 @@ RSpec.describe ContainerRegistry::Migration::EnqueuerWorker, :aggregate_failures
stub_container_registry_tags(
repository: container_repository2.path,
- tags: %w(tag4 tag5 tag6),
+ tags: %w[tag4 tag5 tag6],
with_manifest: true
)
end
@@ -204,7 +204,7 @@ RSpec.describe ContainerRegistry::Migration::EnqueuerWorker, :aggregate_failures
stub_application_setting(container_registry_import_max_tags_count: 0)
# Add 8 tags to the next repository
stub_container_registry_tags(
- repository: container_repository.path, tags: %w(a b c d e f g h), with_manifest: true
+ repository: container_repository.path, tags: %w[a b c d e f g h], with_manifest: true
)
end
diff --git a/spec/workers/environments/auto_recover_worker_spec.rb b/spec/workers/environments/auto_recover_worker_spec.rb
new file mode 100644
index 00000000000..c0acfc9945e
--- /dev/null
+++ b/spec/workers/environments/auto_recover_worker_spec.rb
@@ -0,0 +1,68 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Environments::AutoRecoverWorker, feature_category: :continuous_delivery do
+ include CreateEnvironmentsHelpers
+
+ subject { worker.perform(environment_id) }
+
+ let_it_be(:project) { create(:project, :repository) }
+ let_it_be(:developer) { create(:user).tap { |u| project.add_developer(u) } }
+ let_it_be(:reporter) { create(:user).tap { |u| project.add_reporter(u) } }
+
+ let!(:environment) { create_review_app(user, project, 'review/feature').environment }
+ let(:environment_id) { environment.id }
+ let(:worker) { described_class.new }
+ let(:user) { developer }
+
+ before_all do
+ project.repository.add_branch(developer, 'review/feature', 'master')
+ end
+
+ it 'has the `until_executed` deduplicate strategy' do
+ expect(described_class.get_deduplicate_strategy).to eq(:until_executed)
+ end
+
+ context 'when environment has been updated recently' do
+ it 'recovers the environment' do
+ environment.stop!
+ environment.update!(updated_at: (Environment::LONG_STOP - 1.day).ago)
+
+ expect { subject }
+ .not_to change { environment.reload.state }
+ .from('stopping')
+ end
+ end
+
+ context 'when all stop actions are not complete' do
+ it 'does not recover the environment' do
+ environment.stop!
+ environment.stop_actions.map(&:drop)
+ environment.update!(updated_at: (Environment::LONG_STOP + 1.day).ago)
+
+ expect { subject }
+ .to change { environment.reload.state }
+ .from('stopping').to('available')
+ end
+ end
+
+ context 'when all stop actions are complete' do
+ it 'recovers the environment' do
+ environment.stop!
+ environment.update!(updated_at: (Environment::LONG_STOP + 1.day).ago)
+
+ expect { subject }
+ .not_to change { environment.reload.state }
+ .from('stopping')
+ end
+ end
+
+ context 'when there are no corresponding environment record' do
+ let!(:environment) { instance_double('Environment', id: non_existing_record_id) }
+
+ it 'ignores the invalid record' do
+ expect { subject }.not_to raise_error
+ end
+ end
+end
diff --git a/spec/workers/environments/auto_stop_cron_worker_spec.rb b/spec/workers/environments/auto_stop_cron_worker_spec.rb
index ad44cf97e07..14a74022a1f 100644
--- a/spec/workers/environments/auto_stop_cron_worker_spec.rb
+++ b/spec/workers/environments/auto_stop_cron_worker_spec.rb
@@ -14,4 +14,12 @@ RSpec.describe Environments::AutoStopCronWorker, feature_category: :continuous_d
subject
end
+
+ it 'executes Environments::AutoRecoverService' do
+ expect_next_instance_of(Environments::AutoRecoverService) do |service|
+ expect(service).to receive(:execute)
+ end
+
+ subject
+ end
end
diff --git a/spec/workers/every_sidekiq_worker_spec.rb b/spec/workers/every_sidekiq_worker_spec.rb
index 4855967d462..4c2cff434a7 100644
--- a/spec/workers/every_sidekiq_worker_spec.rb
+++ b/spec/workers/every_sidekiq_worker_spec.rb
@@ -137,11 +137,11 @@ RSpec.describe 'Every Sidekiq worker', feature_category: :shared do
'BuildHooksWorker' => 3,
'BuildQueueWorker' => 3,
'BuildSuccessWorker' => 3,
- 'BulkImportWorker' => false,
+ 'BulkImportWorker' => 3,
'BulkImports::ExportRequestWorker' => 5,
- 'BulkImports::EntityWorker' => false,
- 'BulkImports::PipelineWorker' => false,
- 'BulkImports::PipelineBatchWorker' => false,
+ 'BulkImports::EntityWorker' => 3,
+ 'BulkImports::PipelineWorker' => 3,
+ 'BulkImports::PipelineBatchWorker' => 3,
'BulkImports::FinishProjectImportWorker' => 5,
'Chaos::CpuSpinWorker' => 3,
'Chaos::DbSpinWorker' => 3,
@@ -325,10 +325,6 @@ RSpec.describe 'Every Sidekiq worker', feature_category: :shared do
'Groups::ScheduleBulkRepositoryShardMovesWorker' => 3,
'Groups::UpdateRepositoryStorageWorker' => 3,
'Groups::UpdateStatisticsWorker' => 3,
- 'HashedStorage::MigratorWorker' => 3,
- 'HashedStorage::ProjectMigrateWorker' => 3,
- 'HashedStorage::ProjectRollbackWorker' => 3,
- 'HashedStorage::RollbackerWorker' => 3,
'ImportIssuesCsvWorker' => 3,
'ImportSoftwareLicensesWorker' => 3,
'IncidentManagement::AddSeveritySystemNoteWorker' => 3,
@@ -355,8 +351,6 @@ RSpec.describe 'Every Sidekiq worker', feature_category: :shared do
'Llm::Embedding::GitlabDocumentation::SetEmbeddingsOnTheRecordWorker' => 5,
'Llm::Embedding::GitlabDocumentation::CreateEmptyEmbeddingsRecordsWorker' => 3,
'Llm::Embedding::GitlabDocumentation::CreateDbEmbeddingsPerDocFileWorker' => 5,
- 'Llm::TanukiBot::UpdateWorker' => 1,
- 'Llm::TanukiBot::RecreateRecordsWorker' => 3,
'MailScheduler::IssueDueWorker' => 3,
'MailScheduler::NotificationServiceWorker' => 3,
'MembersDestroyer::UnassignIssuablesWorker' => 3,
@@ -396,6 +390,7 @@ RSpec.describe 'Every Sidekiq worker', feature_category: :shared do
'Packages::Go::SyncPackagesWorker' => 3,
'Packages::MarkPackageFilesForDestructionWorker' => 3,
'Packages::Maven::Metadata::SyncWorker' => 3,
+ 'Packages::Npm::CleanupStaleMetadataCacheWorker' => 0,
'Packages::Nuget::ExtractionWorker' => 3,
'Packages::Rubygems::ExtractionWorker' => 3,
'PagesDomainSslRenewalWorker' => 3,
@@ -486,7 +481,9 @@ RSpec.describe 'Every Sidekiq worker', feature_category: :shared do
'X509CertificateRevokeWorker' => 3,
'ComplianceManagement::MergeRequests::ComplianceViolationsWorker' => 3,
'Zoekt::IndexerWorker' => 2,
- 'Issuable::RelatedLinksCreateWorker' => 3
+ 'Issuable::RelatedLinksCreateWorker' => 3,
+ 'BulkImports::RelationBatchExportWorker' => 3,
+ 'BulkImports::RelationExportWorker' => 3
}.merge(extra_retry_exceptions)
end
diff --git a/spec/workers/gitlab/bitbucket_import/advance_stage_worker_spec.rb b/spec/workers/gitlab/bitbucket_import/advance_stage_worker_spec.rb
index c04ccafdcf8..673988a3275 100644
--- a/spec/workers/gitlab/bitbucket_import/advance_stage_worker_spec.rb
+++ b/spec/workers/gitlab/bitbucket_import/advance_stage_worker_spec.rb
@@ -32,7 +32,7 @@ RSpec.describe Gitlab::BitbucketImport::AdvanceStageWorker, :clean_gitlab_redis_
expect(described_class)
.to receive(:perform_in)
- .with(described_class::INTERVAL, project.id, { '123' => 1 }, :finish, Time.zone.now, 1)
+ .with(described_class::INTERVAL, project.id, { '123' => 1 }, 'finish', Time.zone.now.to_s, 1)
worker.perform(project.id, { '123' => 2 }, :finish)
end
diff --git a/spec/workers/gitlab/github_import/stage/import_attachments_worker_spec.rb b/spec/workers/gitlab/github_import/stage/import_attachments_worker_spec.rb
index 9a4b9106dae..c8b528593b9 100644
--- a/spec/workers/gitlab/github_import/stage/import_attachments_worker_spec.rb
+++ b/spec/workers/gitlab/github_import/stage/import_attachments_worker_spec.rb
@@ -51,7 +51,7 @@ RSpec.describe Gitlab::GithubImport::Stage::ImportAttachmentsWorker, feature_cat
expect(Gitlab::GithubImport::AdvanceStageWorker)
.to receive(:perform_async)
- .with(project.id, { '123' => 2, '234' => 3, '345' => 4, '456' => 5 }, :protected_branches)
+ .with(project.id, { '123' => 2, '234' => 3, '345' => 4, '456' => 5 }, 'protected_branches')
worker.import(client, project)
end
@@ -62,7 +62,7 @@ RSpec.describe Gitlab::GithubImport::Stage::ImportAttachmentsWorker, feature_cat
it 'skips release attachments import and calls next stage' do
importers.each { |importer| expect(importer[:klass]).not_to receive(:new) }
expect(Gitlab::GithubImport::AdvanceStageWorker)
- .to receive(:perform_async).with(project.id, {}, :protected_branches)
+ .to receive(:perform_async).with(project.id, {}, 'protected_branches')
worker.import(client, project)
end
diff --git a/spec/workers/gitlab/github_import/stage/import_base_data_worker_spec.rb b/spec/workers/gitlab/github_import/stage/import_base_data_worker_spec.rb
index f3b706361e3..b8f2db8e2d9 100644
--- a/spec/workers/gitlab/github_import/stage/import_base_data_worker_spec.rb
+++ b/spec/workers/gitlab/github_import/stage/import_base_data_worker_spec.rb
@@ -23,8 +23,6 @@ RSpec.describe Gitlab::GithubImport::Stage::ImportBaseDataWorker, feature_catego
expect(importer).to receive(:execute)
end
- expect(import_state).to receive(:refresh_jid_expiration)
-
expect(Gitlab::GithubImport::Stage::ImportPullRequestsWorker)
.to receive(:perform_async)
.with(project.id)
diff --git a/spec/workers/gitlab/github_import/stage/import_collaborators_worker_spec.rb b/spec/workers/gitlab/github_import/stage/import_collaborators_worker_spec.rb
index fc38adb5447..6a55f575da8 100644
--- a/spec/workers/gitlab/github_import/stage/import_collaborators_worker_spec.rb
+++ b/spec/workers/gitlab/github_import/stage/import_collaborators_worker_spec.rb
@@ -33,11 +33,9 @@ RSpec.describe Gitlab::GithubImport::Stage::ImportCollaboratorsWorker, feature_c
.and_return(importer)
expect(importer).to receive(:execute).and_return(waiter)
- expect(import_state).to receive(:refresh_jid_expiration)
-
expect(Gitlab::GithubImport::AdvanceStageWorker)
.to receive(:perform_async)
- .with(project.id, { '123' => 2 }, :pull_requests_merged_by)
+ .with(project.id, { '123' => 2 }, 'pull_requests_merged_by')
worker.import(client, project)
end
@@ -51,7 +49,7 @@ RSpec.describe Gitlab::GithubImport::Stage::ImportCollaboratorsWorker, feature_c
expect(Gitlab::GithubImport::AdvanceStageWorker)
.to receive(:perform_async)
- .with(project.id, {}, :pull_requests_merged_by)
+ .with(project.id, {}, 'pull_requests_merged_by')
worker.import(client, project)
end
@@ -65,7 +63,7 @@ RSpec.describe Gitlab::GithubImport::Stage::ImportCollaboratorsWorker, feature_c
expect(Gitlab::GithubImport::AdvanceStageWorker)
.to receive(:perform_async)
- .with(project.id, {}, :pull_requests_merged_by)
+ .with(project.id, {}, 'pull_requests_merged_by')
worker.import(client, project)
end
diff --git a/spec/workers/gitlab/github_import/stage/import_issue_events_worker_spec.rb b/spec/workers/gitlab/github_import/stage/import_issue_events_worker_spec.rb
index 4b4d6a5b625..bad3a5beb0e 100644
--- a/spec/workers/gitlab/github_import/stage/import_issue_events_worker_spec.rb
+++ b/spec/workers/gitlab/github_import/stage/import_issue_events_worker_spec.rb
@@ -33,7 +33,7 @@ RSpec.describe Gitlab::GithubImport::Stage::ImportIssueEventsWorker, feature_cat
expect(Gitlab::GithubImport::AdvanceStageWorker)
.to receive(:perform_async)
- .with(project.id, { '123' => 2 }, :notes)
+ .with(project.id, { '123' => 2 }, 'notes')
worker.import(client, project)
end
@@ -44,7 +44,7 @@ RSpec.describe Gitlab::GithubImport::Stage::ImportIssueEventsWorker, feature_cat
it 'skips issue events import and calls next stage' do
expect(Gitlab::GithubImport::Importer::SingleEndpointIssueEventsImporter).not_to receive(:new)
- expect(Gitlab::GithubImport::AdvanceStageWorker).to receive(:perform_async).with(project.id, {}, :notes)
+ expect(Gitlab::GithubImport::AdvanceStageWorker).to receive(:perform_async).with(project.id, {}, 'notes')
worker.import(client, project)
end
diff --git a/spec/workers/gitlab/github_import/stage/import_issues_and_diff_notes_worker_spec.rb b/spec/workers/gitlab/github_import/stage/import_issues_and_diff_notes_worker_spec.rb
index 7a5813122f4..10f6ebfbab9 100644
--- a/spec/workers/gitlab/github_import/stage/import_issues_and_diff_notes_worker_spec.rb
+++ b/spec/workers/gitlab/github_import/stage/import_issues_and_diff_notes_worker_spec.rb
@@ -35,7 +35,7 @@ RSpec.describe Gitlab::GithubImport::Stage::ImportIssuesAndDiffNotesWorker, feat
expect(Gitlab::GithubImport::AdvanceStageWorker)
.to receive(:perform_async)
- .with(project.id, { '123' => 2 }, :issue_events)
+ .with(project.id, { '123' => 2 }, 'issue_events')
worker.import(client, project)
end
diff --git a/spec/workers/gitlab/github_import/stage/import_lfs_objects_worker_spec.rb b/spec/workers/gitlab/github_import/stage/import_lfs_objects_worker_spec.rb
index 5d476543743..40194a91b3a 100644
--- a/spec/workers/gitlab/github_import/stage/import_lfs_objects_worker_spec.rb
+++ b/spec/workers/gitlab/github_import/stage/import_lfs_objects_worker_spec.rb
@@ -24,7 +24,7 @@ RSpec.describe Gitlab::GithubImport::Stage::ImportLfsObjectsWorker, feature_cate
expect(Gitlab::GithubImport::AdvanceStageWorker)
.to receive(:perform_async)
- .with(project.id, { '123' => 2 }, :finish)
+ .with(project.id, { '123' => 2 }, 'finish')
worker.import(project)
end
diff --git a/spec/workers/gitlab/github_import/stage/import_notes_worker_spec.rb b/spec/workers/gitlab/github_import/stage/import_notes_worker_spec.rb
index 9584708802a..69078a666a5 100644
--- a/spec/workers/gitlab/github_import/stage/import_notes_worker_spec.rb
+++ b/spec/workers/gitlab/github_import/stage/import_notes_worker_spec.rb
@@ -35,7 +35,7 @@ RSpec.describe Gitlab::GithubImport::Stage::ImportNotesWorker, feature_category:
expect(Gitlab::GithubImport::AdvanceStageWorker)
.to receive(:perform_async)
- .with(project.id, { '123' => 2 }, :attachments)
+ .with(project.id, { '123' => 2 }, 'attachments')
worker.import(client, project)
end
diff --git a/spec/workers/gitlab/github_import/stage/import_protected_branches_worker_spec.rb b/spec/workers/gitlab/github_import/stage/import_protected_branches_worker_spec.rb
index 7ecce82dacb..b73f8c6524d 100644
--- a/spec/workers/gitlab/github_import/stage/import_protected_branches_worker_spec.rb
+++ b/spec/workers/gitlab/github_import/stage/import_protected_branches_worker_spec.rb
@@ -25,12 +25,9 @@ RSpec.describe Gitlab::GithubImport::Stage::ImportProtectedBranchesWorker, featu
.to receive(:execute)
.and_return(waiter)
- expect(import_state)
- .to receive(:refresh_jid_expiration)
-
expect(Gitlab::GithubImport::AdvanceStageWorker)
.to receive(:perform_async)
- .with(project.id, { '123' => 2 }, :lfs_objects)
+ .with(project.id, { '123' => 2 }, 'lfs_objects')
worker.import(client, project)
end
diff --git a/spec/workers/gitlab/github_import/stage/import_pull_requests_merged_by_worker_spec.rb b/spec/workers/gitlab/github_import/stage/import_pull_requests_merged_by_worker_spec.rb
index 5917b827d65..b214f6a97d4 100644
--- a/spec/workers/gitlab/github_import/stage/import_pull_requests_merged_by_worker_spec.rb
+++ b/spec/workers/gitlab/github_import/stage/import_pull_requests_merged_by_worker_spec.rb
@@ -24,12 +24,9 @@ RSpec.describe Gitlab::GithubImport::Stage::ImportPullRequestsMergedByWorker, fe
.to receive(:execute)
.and_return(waiter)
- expect(import_state)
- .to receive(:refresh_jid_expiration)
-
expect(Gitlab::GithubImport::AdvanceStageWorker)
.to receive(:perform_async)
- .with(project.id, { '123' => 2 }, :pull_request_review_requests)
+ .with(project.id, { '123' => 2 }, 'pull_request_review_requests')
worker.import(client, project)
end
diff --git a/spec/workers/gitlab/github_import/stage/import_pull_requests_review_requests_worker_spec.rb b/spec/workers/gitlab/github_import/stage/import_pull_requests_review_requests_worker_spec.rb
index b473de73086..4468de7e691 100644
--- a/spec/workers/gitlab/github_import/stage/import_pull_requests_review_requests_worker_spec.rb
+++ b/spec/workers/gitlab/github_import/stage/import_pull_requests_review_requests_worker_spec.rb
@@ -6,7 +6,7 @@ RSpec.describe Gitlab::GithubImport::Stage::ImportPullRequestsReviewRequestsWork
subject(:worker) { described_class.new }
let(:project) { instance_double(Project, id: 1, import_state: import_state) }
- let(:import_state) { instance_double(ProjectImportState, refresh_jid_expiration: true) }
+ let(:import_state) { instance_double(ProjectImportState) }
let(:client) { instance_double(Gitlab::GithubImport::Client) }
let(:importer) { instance_double(Gitlab::GithubImport::Importer::PullRequests::ReviewRequestsImporter) }
let(:waiter) { Gitlab::JobWaiter.new(2, '123') }
@@ -21,11 +21,10 @@ RSpec.describe Gitlab::GithubImport::Stage::ImportPullRequestsReviewRequestsWork
.and_return(importer)
expect(importer).to receive(:execute).and_return(waiter)
- expect(import_state).to receive(:refresh_jid_expiration)
expect(Gitlab::GithubImport::AdvanceStageWorker)
.to receive(:perform_async)
- .with(project.id, { '123' => 2 }, :pull_request_reviews)
+ .with(project.id, { '123' => 2 }, 'pull_request_reviews')
worker.import(client, project)
end
diff --git a/spec/workers/gitlab/github_import/stage/import_pull_requests_reviews_worker_spec.rb b/spec/workers/gitlab/github_import/stage/import_pull_requests_reviews_worker_spec.rb
index 34d3ce9fe95..48b41435adb 100644
--- a/spec/workers/gitlab/github_import/stage/import_pull_requests_reviews_worker_spec.rb
+++ b/spec/workers/gitlab/github_import/stage/import_pull_requests_reviews_worker_spec.rb
@@ -25,11 +25,9 @@ RSpec.describe Gitlab::GithubImport::Stage::ImportPullRequestsReviewsWorker, fea
.to receive(:execute)
.and_return(waiter)
- expect(import_state).to receive(:refresh_jid_expiration)
-
expect(Gitlab::GithubImport::AdvanceStageWorker)
.to receive(:perform_async)
- .with(project.id, { '123' => 2 }, :issues_and_diff_notes)
+ .with(project.id, { '123' => 2 }, 'issues_and_diff_notes')
worker.import(client, project)
end
diff --git a/spec/workers/gitlab/github_import/stage/import_pull_requests_worker_spec.rb b/spec/workers/gitlab/github_import/stage/import_pull_requests_worker_spec.rb
index f9b4a8a99f0..2ea66d8cdf3 100644
--- a/spec/workers/gitlab/github_import/stage/import_pull_requests_worker_spec.rb
+++ b/spec/workers/gitlab/github_import/stage/import_pull_requests_worker_spec.rb
@@ -27,9 +27,6 @@ RSpec.describe Gitlab::GithubImport::Stage::ImportPullRequestsWorker, feature_ca
.to receive(:execute)
.and_return(waiter)
- expect(import_state)
- .to receive(:refresh_jid_expiration)
-
expect(InternalId).to receive(:exists?).and_return(false)
expect(client).to receive(:each_object).with(
@@ -38,7 +35,7 @@ RSpec.describe Gitlab::GithubImport::Stage::ImportPullRequestsWorker, feature_ca
expect(Gitlab::GithubImport::AdvanceStageWorker)
.to receive(:perform_async)
- .with(project.id, { '123' => 2 }, :collaborators)
+ .with(project.id, { '123' => 2 }, 'collaborators')
expect(MergeRequest).to receive(:track_target_project_iid!)
@@ -59,9 +56,6 @@ RSpec.describe Gitlab::GithubImport::Stage::ImportPullRequestsWorker, feature_ca
.to receive(:execute)
.and_return(waiter)
- expect(import_state)
- .to receive(:refresh_jid_expiration)
-
expect(InternalId).to receive(:exists?).and_return(false)
expect(client).to receive(:each_object).with(
@@ -70,7 +64,7 @@ RSpec.describe Gitlab::GithubImport::Stage::ImportPullRequestsWorker, feature_ca
expect(Gitlab::GithubImport::AdvanceStageWorker)
.to receive(:perform_async)
- .with(project.id, { '123' => 2 }, :collaborators)
+ .with(project.id, { '123' => 2 }, 'collaborators')
expect(MergeRequest).not_to receive(:track_target_project_iid!)
@@ -91,9 +85,6 @@ RSpec.describe Gitlab::GithubImport::Stage::ImportPullRequestsWorker, feature_ca
.to receive(:execute)
.and_return(waiter)
- expect(import_state)
- .to receive(:refresh_jid_expiration)
-
expect(InternalId).to receive(:exists?).and_return(true)
expect(client).not_to receive(:each_object)
diff --git a/spec/workers/gitlab/jira_import/stage/import_issues_worker_spec.rb b/spec/workers/gitlab/jira_import/stage/import_issues_worker_spec.rb
index 594f9618770..f9b03fc1b44 100644
--- a/spec/workers/gitlab/jira_import/stage/import_issues_worker_spec.rb
+++ b/spec/workers/gitlab/jira_import/stage/import_issues_worker_spec.rb
@@ -25,7 +25,11 @@ RSpec.describe Gitlab::JiraImport::Stage::ImportIssuesWorker, feature_category:
end
context 'when import started', :clean_gitlab_redis_cache do
- let_it_be(:jira_integration) { create(:jira_integration, project: project) }
+ let(:job_waiter) { Gitlab::JobWaiter.new(2, 'some-job-key') }
+
+ before_all do
+ create(:jira_integration, project: project)
+ end
before do
jira_import.start!
@@ -34,6 +38,40 @@ RSpec.describe Gitlab::JiraImport::Stage::ImportIssuesWorker, feature_category:
end
end
+ it 'uses a custom http client for the issues importer' do
+ jira_integration = project.jira_integration
+ client = instance_double(JIRA::Client)
+ issue_importer = instance_double(Gitlab::JiraImport::IssuesImporter)
+
+ allow(Project).to receive(:find_by_id).with(project.id).and_return(project)
+ allow(issue_importer).to receive(:execute).and_return(job_waiter)
+
+ expect(jira_integration).to receive(:client).with(read_timeout: 2.minutes).and_return(client)
+ expect(Gitlab::JiraImport::IssuesImporter).to receive(:new).with(
+ project,
+ client
+ ).and_return(issue_importer)
+
+ described_class.new.perform(project.id)
+ end
+
+ context 'when increase_jira_import_issues_timeout feature flag is disabled' do
+ before do
+ stub_feature_flags(increase_jira_import_issues_timeout: false)
+ end
+
+ it 'does not provide a custom client to IssuesImporter' do
+ issue_importer = instance_double(Gitlab::JiraImport::IssuesImporter)
+ expect(Gitlab::JiraImport::IssuesImporter).to receive(:new).with(
+ instance_of(Project),
+ nil
+ ).and_return(issue_importer)
+ allow(issue_importer).to receive(:execute).and_return(job_waiter)
+
+ described_class.new.perform(project.id)
+ end
+ end
+
context 'when start_at is nil' do
it_behaves_like 'advance to next stage', :attachments
end
diff --git a/spec/workers/groups/update_statistics_worker_spec.rb b/spec/workers/groups/update_statistics_worker_spec.rb
index f47606f0580..5fc4ccdab0d 100644
--- a/spec/workers/groups/update_statistics_worker_spec.rb
+++ b/spec/workers/groups/update_statistics_worker_spec.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe Groups::UpdateStatisticsWorker, feature_category: :source_code_management do
let_it_be(:group) { create(:group) }
- let(:statistics) { %w(wiki_size) }
+ let(:statistics) { %w[wiki_size] }
subject(:worker) { described_class.new }
diff --git a/spec/workers/jira_connect/sync_branch_worker_spec.rb b/spec/workers/jira_connect/sync_branch_worker_spec.rb
index 1c2661ad0e5..18eb22b8a47 100644
--- a/spec/workers/jira_connect/sync_branch_worker_spec.rb
+++ b/spec/workers/jira_connect/sync_branch_worker_spec.rb
@@ -14,7 +14,7 @@ RSpec.describe JiraConnect::SyncBranchWorker, feature_category: :integrations do
let(:project_id) { project.id }
let(:branch_name) { 'master' }
- let(:commit_shas) { %w(b83d6e3 5a62481) }
+ let(:commit_shas) { %w[b83d6e3 5a62481] }
let(:update_sequence_id) { 1 }
def perform
diff --git a/spec/workers/merge_request_cleanup_refs_worker_spec.rb b/spec/workers/merge_request_cleanup_refs_worker_spec.rb
index a2df31037be..6c87b6827a8 100644
--- a/spec/workers/merge_request_cleanup_refs_worker_spec.rb
+++ b/spec/workers/merge_request_cleanup_refs_worker_spec.rb
@@ -40,18 +40,6 @@ RSpec.describe MergeRequestCleanupRefsWorker, feature_category: :code_review_wor
end
end
end
-
- context 'when merge_request_refs_cleanup flag is disabled' do
- before do
- stub_feature_flags(merge_request_refs_cleanup: false)
- end
-
- it 'does nothing' do
- expect(MergeRequests::CleanupRefsService).not_to receive(:new)
-
- worker.perform_work
- end
- end
end
context 'when there is no next cleanup schedule found' do
diff --git a/spec/workers/merge_requests/set_reviewer_reviewed_worker_spec.rb b/spec/workers/merge_requests/set_reviewer_reviewed_worker_spec.rb
index 942cf8e87e9..7341a0dcc5b 100644
--- a/spec/workers/merge_requests/set_reviewer_reviewed_worker_spec.rb
+++ b/spec/workers/merge_requests/set_reviewer_reviewed_worker_spec.rb
@@ -14,21 +14,21 @@ RSpec.describe MergeRequests::SetReviewerReviewedWorker, feature_category: :sour
let(:event) { approved_event }
end
- it 'calls MergeRequests::MarkReviewerReviewedService' do
+ it 'calls MergeRequests::UpdateReviewerStateService' do
expect_next_instance_of(
- MergeRequests::MarkReviewerReviewedService,
+ MergeRequests::UpdateReviewerStateService,
project: project, current_user: user
) do |service|
- expect(service).to receive(:execute).with(merge_request)
+ expect(service).to receive(:execute).with(merge_request, "reviewed")
end
consume_event(subscriber: described_class, event: approved_event)
end
shared_examples 'when object does not exist' do
- it 'logs and does not call MergeRequests::MarkReviewerReviewedService' do
+ it 'logs and does not call MergeRequests::UpdateReviewerStateService' do
expect(Sidekiq.logger).to receive(:info).with(hash_including(log_payload))
- expect(MergeRequests::MarkReviewerReviewedService).not_to receive(:new)
+ expect(MergeRequests::UpdateReviewerStateService).not_to receive(:new)
expect { consume_event(subscriber: described_class, event: approved_event) }
.not_to raise_exception
diff --git a/spec/workers/packages/cleanup_package_registry_worker_spec.rb b/spec/workers/packages/cleanup_package_registry_worker_spec.rb
index f70103070ef..f2787a92fbf 100644
--- a/spec/workers/packages/cleanup_package_registry_worker_spec.rb
+++ b/spec/workers/packages/cleanup_package_registry_worker_spec.rb
@@ -58,6 +58,28 @@ RSpec.describe Packages::CleanupPackageRegistryWorker, feature_category: :packag
end
end
+ context 'with npm metadata caches pending destruction' do
+ let_it_be(:npm_metadata_cache) { create(:npm_metadata_cache, :stale) }
+
+ it_behaves_like 'an idempotent worker'
+
+ it 'queues the cleanup job' do
+ expect(Packages::Npm::CleanupStaleMetadataCacheWorker).to receive(:perform_with_capacity)
+
+ perform
+ end
+ end
+
+ context 'with no npm metadata caches pending destruction' do
+ it_behaves_like 'an idempotent worker'
+
+ it 'does not queue the cleanup job' do
+ expect(Packages::Npm::CleanupStaleMetadataCacheWorker).not_to receive(:perform_with_capacity)
+
+ perform
+ end
+ end
+
describe 'counts logging' do
let_it_be(:processing_package_file) { create(:package_file, status: :processing) }
diff --git a/spec/workers/packages/npm/cleanup_stale_metadata_cache_worker_spec.rb b/spec/workers/packages/npm/cleanup_stale_metadata_cache_worker_spec.rb
new file mode 100644
index 00000000000..390ed0ee453
--- /dev/null
+++ b/spec/workers/packages/npm/cleanup_stale_metadata_cache_worker_spec.rb
@@ -0,0 +1,79 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Packages::Npm::CleanupStaleMetadataCacheWorker, type: :worker, feature_category: :package_registry do
+ 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(:npm_metadata_cache1) { create(:npm_metadata_cache) }
+ let_it_be(:npm_metadata_cache2) { create(:npm_metadata_cache, :stale) }
+
+ let_it_be(:npm_metadata_cache3) do
+ create(:npm_metadata_cache, :stale, updated_at: 1.year.ago, created_at: 1.year.ago)
+ end
+
+ it 'deletes the oldest stale metadata cache based on id', :aggregate_failures do
+ expect(worker).to receive(:log_extra_metadata_on_done).with(:npm_metadata_cache_id, npm_metadata_cache2.id)
+
+ expect { subject }.to change { Packages::Npm::MetadataCache.count }.by(-1)
+ expect { npm_metadata_cache2.reload }.to raise_error(ActiveRecord::RecordNotFound)
+ end
+ end
+
+ context 'with a stale metadata cache' do
+ let_it_be(:npm_metadata_cache) { create(:npm_metadata_cache, :stale) }
+
+ context 'with an error during the destroy' do
+ before do
+ allow_next_found_instance_of(Packages::Npm::MetadataCache) do |metadata_cache|
+ allow(metadata_cache).to receive(:destroy!).and_raise('Error!')
+ end
+ end
+
+ it 'handles the error' do
+ expect(Gitlab::ErrorTracking).to receive(:log_exception)
+ .with(instance_of(RuntimeError), class: described_class.name)
+ expect { subject }.to change { Packages::Npm::MetadataCache.error.count }.from(0).to(1)
+ expect(npm_metadata_cache.reload).to be_error
+ end
+ end
+
+ context 'when trying to destroy a destroyed record' do
+ before do
+ allow_next_found_instance_of(Packages::Npm::MetadataCache) do |metadata_cache|
+ destroy_method = metadata_cache.method(:destroy!)
+
+ allow(metadata_cache).to receive(:destroy!) do
+ destroy_method.call
+
+ raise 'Error!'
+ end
+ end
+ end
+
+ it 'handles the error' do
+ expect(Gitlab::ErrorTracking).to receive(:log_exception)
+ .with(instance_of(RuntimeError), class: described_class.name)
+ expect { subject }.not_to change { Packages::Npm::MetadataCache.count }
+ expect(npm_metadata_cache.reload).to be_error
+ end
+ end
+ end
+ end
+
+ describe '#max_running_jobs' do
+ let(:capacity) { described_class::MAX_CAPACITY }
+
+ subject { worker.max_running_jobs }
+
+ it { is_expected.to eq(capacity) }
+ end
+end
diff --git a/spec/workers/post_receive_spec.rb b/spec/workers/post_receive_spec.rb
index 2e0a2535453..dcece830a85 100644
--- a/spec/workers/post_receive_spec.rb
+++ b/spec/workers/post_receive_spec.rb
@@ -391,7 +391,7 @@ RSpec.describe PostReceive, feature_category: :source_code_management do
it 'enqueues a UpdateMergeRequestsWorker job' do
allow(Project).to receive(:find_by).and_return(project)
- expect_next(MergeRequests::PushedBranchesService).to receive(:execute).and_return(%w(tést))
+ expect_next(MergeRequests::PushedBranchesService).to receive(:execute).and_return(%w[tést])
expect(UpdateMergeRequestsWorker).to receive(:perform_async).with(project.id, project.first_owner.id, any_args)
diff --git a/spec/workers/project_cache_worker_spec.rb b/spec/workers/project_cache_worker_spec.rb
index 4d468897599..7ef2494b5cf 100644
--- a/spec/workers/project_cache_worker_spec.rb
+++ b/spec/workers/project_cache_worker_spec.rb
@@ -35,10 +35,10 @@ RSpec.describe ProjectCacheWorker, feature_category: :source_code_management do
context 'with an existing project' do
it 'refreshes the method caches' do
expect_any_instance_of(Repository).to receive(:refresh_method_caches)
- .with(%i(readme))
+ .with(%i[readme])
.and_call_original
- worker.perform(project.id, %w(readme))
+ worker.perform(project.id, %w[readme])
end
context 'with statistics disabled' do
@@ -52,7 +52,7 @@ RSpec.describe ProjectCacheWorker, feature_category: :source_code_management do
end
context 'with statistics' do
- let(:statistics) { %w(repository_size) }
+ let(:statistics) { %w[repository_size] }
it 'updates the project statistics' do
expect(worker).to receive(:update_statistics)
@@ -69,16 +69,16 @@ RSpec.describe ProjectCacheWorker, feature_category: :source_code_management do
allow(Gitlab::MarkupHelper).to receive(:plain?).and_return(true)
expect_any_instance_of(Repository).to receive(:refresh_method_caches)
- .with(%i(readme))
+ .with(%i[readme])
.and_call_original
- worker.perform(project.id, %w(readme))
+ worker.perform(project.id, %w[readme])
end
end
end
end
describe '#update_statistics' do
- let(:statistics) { %w(repository_size) }
+ let(:statistics) { %w[repository_size] }
context 'when a lease could not be obtained' do
it 'does not update the project statistics' do
@@ -120,7 +120,7 @@ RSpec.describe ProjectCacheWorker, feature_category: :source_code_management do
end
it_behaves_like 'an idempotent worker' do
- let(:job_args) { [project.id, %w(readme), %w(repository_size)] }
+ let(:job_args) { [project.id, %w[readme], %w[repository_size]] }
it 'calls Projects::UpdateStatisticsService service twice', :clean_gitlab_redis_shared_state do
expect(Projects::UpdateStatisticsService).to receive(:new).once.and_return(double(execute: true))
diff --git a/spec/workers/projects/import_export/after_import_merge_requests_worker_spec.rb b/spec/workers/projects/import_export/after_import_merge_requests_worker_spec.rb
new file mode 100644
index 00000000000..42b67a0941a
--- /dev/null
+++ b/spec/workers/projects/import_export/after_import_merge_requests_worker_spec.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Projects::ImportExport::AfterImportMergeRequestsWorker, feature_category: :importers do
+ let_it_be(:project) { create(:project) }
+ let_it_be(:merge_requests) { project.merge_requests }
+
+ let(:worker) { described_class.new }
+
+ describe '#perform' do
+ it 'sets the latest merge request diff ids' do
+ expect(project.class).to receive(:find_by_id).and_return(project)
+ expect(merge_requests).to receive(:set_latest_merge_request_diff_ids!)
+
+ worker.perform(project.id)
+ end
+
+ it_behaves_like 'an idempotent worker' do
+ let(:job_args) { [project.id] }
+ end
+ end
+end
diff --git a/spec/workers/projects/record_target_platforms_worker_spec.rb b/spec/workers/projects/record_target_platforms_worker_spec.rb
index d4515f7727a..23705d0c86e 100644
--- a/spec/workers/projects/record_target_platforms_worker_spec.rb
+++ b/spec/workers/projects/record_target_platforms_worker_spec.rb
@@ -10,7 +10,7 @@ RSpec.describe Projects::RecordTargetPlatformsWorker, feature_category: :activat
let_it_be(:project) { create(:project, :repository, detected_repository_languages: true) }
let(:worker) { described_class.new }
- let(:service_result) { %w(ios osx watchos) }
+ let(:service_result) { %w[ios osx watchos] }
let(:service_double) { instance_double(Projects::RecordTargetPlatformsService, execute: service_result) }
let(:lease_key) { "#{described_class.name.underscore}:#{project.id}" }
let(:lease_timeout) { described_class::LEASE_TIMEOUT }
diff --git a/spec/workers/repository_fork_worker_spec.rb b/spec/workers/repository_fork_worker_spec.rb
index 3a5528b6a04..bd452c21d9a 100644
--- a/spec/workers/repository_fork_worker_spec.rb
+++ b/spec/workers/repository_fork_worker_spec.rb
@@ -19,11 +19,11 @@ RSpec.describe RepositoryForkWorker, feature_category: :source_code_management d
fork_project(project, forked_project.creator, target_project: forked_project, repository: true)
end
- shared_examples 'RepositoryForkWorker performing' do
- def expect_fork_repository(success:)
+ shared_examples 'RepositoryForkWorker performing' do |branch|
+ def expect_fork_repository(success:, branch:)
allow(::Gitlab::GitalyClient::RepositoryService).to receive(:new).and_call_original
expect_next_instance_of(::Gitlab::GitalyClient::RepositoryService, forked_project.repository.raw) do |svc|
- exp = expect(svc).to receive(:fork_repository).with(project.repository.raw)
+ exp = expect(svc).to receive(:fork_repository).with(project.repository.raw, branch)
if success
exp.and_return(true)
@@ -39,20 +39,20 @@ RSpec.describe RepositoryForkWorker, feature_category: :source_code_management d
it 'creates a new repository from a fork' do
allow(subject).to receive(:jid).and_return(jid)
- expect_fork_repository(success: true)
+ expect_fork_repository(success: true, branch: branch)
perform!
end
end
it "creates a new repository from a fork" do
- expect_fork_repository(success: true)
+ expect_fork_repository(success: true, branch: branch)
perform!
end
it 'protects the default branch' do
- expect_fork_repository(success: true)
+ expect_fork_repository(success: true, branch: branch)
perform!
@@ -60,7 +60,7 @@ RSpec.describe RepositoryForkWorker, feature_category: :source_code_management d
end
it 'flushes various caches' do
- expect_fork_repository(success: true)
+ expect_fork_repository(success: true, branch: branch)
# Works around https://github.com/rspec/rspec-mocks/issues/910
expect(Project).to receive(:find).with(forked_project.id).and_return(forked_project)
@@ -79,13 +79,13 @@ RSpec.describe RepositoryForkWorker, feature_category: :source_code_management d
it 'handles bad fork' do
error_message = "Unable to fork project #{forked_project.id} for repository #{project.disk_path} -> #{forked_project.disk_path}: Failed to create fork repository"
- expect_fork_repository(success: false)
+ expect_fork_repository(success: false, branch: branch)
expect { perform! }.to raise_error(StandardError, error_message)
end
it 'calls Projects::LfsPointers::LfsLinkService#execute with OIDs of source project LFS objects' do
- expect_fork_repository(success: true)
+ expect_fork_repository(success: true, branch: branch)
expect_next_instance_of(Projects::LfsPointers::LfsLinkService) do |service|
expect(service).to receive(:execute).with(project.lfs_objects_oids)
end
@@ -96,7 +96,7 @@ RSpec.describe RepositoryForkWorker, feature_category: :source_code_management d
it "handles LFS objects link failure" do
error_message = "Unable to fork project #{forked_project.id} for repository #{project.disk_path} -> #{forked_project.disk_path}: Source project has too many LFS objects"
- expect_fork_repository(success: true)
+ expect_fork_repository(success: true, branch: branch)
expect_next_instance_of(Projects::LfsPointers::LfsLinkService) do |service|
expect(service).to receive(:execute).and_raise(Projects::LfsPointers::LfsLinkService::TooManyOidsError)
end
@@ -113,6 +113,16 @@ RSpec.describe RepositoryForkWorker, feature_category: :source_code_management d
it_behaves_like 'RepositoryForkWorker performing'
end
+ context 'when a specific branch is requested' do
+ def perform!
+ forked_project.create_import_data(data: { fork_branch: 'wip' })
+
+ subject.perform(forked_project.id)
+ end
+
+ it_behaves_like 'RepositoryForkWorker performing', 'wip'
+ end
+
context 'project ID, storage and repo paths passed' do
def perform!
subject.perform(forked_project.id, 'repos/path', project.disk_path)
diff --git a/spec/workers/schedule_merge_request_cleanup_refs_worker_spec.rb b/spec/workers/schedule_merge_request_cleanup_refs_worker_spec.rb
index b93202fe9b3..173374b02a5 100644
--- a/spec/workers/schedule_merge_request_cleanup_refs_worker_spec.rb
+++ b/spec/workers/schedule_merge_request_cleanup_refs_worker_spec.rb
@@ -13,18 +13,6 @@ RSpec.describe ScheduleMergeRequestCleanupRefsWorker, feature_category: :code_re
worker.perform
end
- context 'when merge_request_refs_cleanup flag is disabled' do
- before do
- stub_feature_flags(merge_request_refs_cleanup: false)
- end
-
- it 'does not schedule any merge request clean ups' do
- expect(MergeRequestCleanupRefsWorker).not_to receive(:perform_with_capacity)
-
- worker.perform
- end
- end
-
it 'retries stuck cleanup schedules' do
expect(MergeRequest::CleanupSchedule).to receive(:stuck_retry!)
diff --git a/spec/workers/stuck_merge_jobs_worker_spec.rb b/spec/workers/stuck_merge_jobs_worker_spec.rb
index 44dc6550cdb..03f371ab740 100644
--- a/spec/workers/stuck_merge_jobs_worker_spec.rb
+++ b/spec/workers/stuck_merge_jobs_worker_spec.rb
@@ -8,7 +8,7 @@ RSpec.describe StuckMergeJobsWorker, feature_category: :code_review_workflow do
context 'merge job identified as completed' do
it 'updates merge request to merged when locked but has merge_commit_sha' do
- allow(Gitlab::SidekiqStatus).to receive(:completed_jids).and_return(%w(123 456))
+ allow(Gitlab::SidekiqStatus).to receive(:completed_jids).and_return(%w[123 456])
mr_with_sha = create(:merge_request, :locked, merge_jid: '123', state: :locked, merge_commit_sha: 'foo-bar-baz')
mr_without_sha = create(:merge_request, :locked, merge_jid: '123', state: :locked, merge_commit_sha: nil)
@@ -23,7 +23,7 @@ RSpec.describe StuckMergeJobsWorker, feature_category: :code_review_workflow do
end
it 'updates merge request to opened when locked but has not been merged', :sidekiq_might_not_need_inline do
- allow(Gitlab::SidekiqStatus).to receive(:completed_jids).and_return(%w(123))
+ allow(Gitlab::SidekiqStatus).to receive(:completed_jids).and_return(%w[123])
merge_request = create(:merge_request, :locked, merge_jid: '123', state: :locked)
pipeline = create(:ci_empty_pipeline, project: merge_request.project, ref: merge_request.source_branch, sha: merge_request.source_branch_sha)
@@ -35,7 +35,7 @@ RSpec.describe StuckMergeJobsWorker, feature_category: :code_review_workflow do
end
it 'logs updated stuck merge job ids' do
- allow(Gitlab::SidekiqStatus).to receive(:completed_jids).and_return(%w(123 456))
+ allow(Gitlab::SidekiqStatus).to receive(:completed_jids).and_return(%w[123 456])
create(:merge_request, :locked, merge_jid: '123')
create(:merge_request, :locked, merge_jid: '456')
diff --git a/spec/workers/tasks_to_be_done/create_worker_spec.rb b/spec/workers/tasks_to_be_done/create_worker_spec.rb
deleted file mode 100644
index 3a4e10b6a6f..00000000000
--- a/spec/workers/tasks_to_be_done/create_worker_spec.rb
+++ /dev/null
@@ -1,22 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe TasksToBeDone::CreateWorker, feature_category: :onboarding do
- let_it_be(:current_user) { create(:user) }
-
- let(:assignee_ids) { [1, 2] }
- let(:job_args) { [123, current_user.id, assignee_ids] }
-
- describe '.perform' do
- it 'executes the task services for all tasks to be done', :aggregate_failures do
- expect { described_class.new.perform(*job_args) }.not_to change { Issue.count }
- end
- end
-
- include_examples 'an idempotent worker' do
- it 'creates 3 task issues' do
- expect { subject }.not_to change { Issue.count }
- end
- end
-end
diff --git a/spec/workers/update_project_statistics_worker_spec.rb b/spec/workers/update_project_statistics_worker_spec.rb
index c5e6f45a201..ba0834e64dc 100644
--- a/spec/workers/update_project_statistics_worker_spec.rb
+++ b/spec/workers/update_project_statistics_worker_spec.rb
@@ -7,7 +7,7 @@ RSpec.describe UpdateProjectStatisticsWorker, feature_category: :source_code_man
let(:worker) { described_class.new }
let(:project) { create(:project, :repository) }
- let(:statistics) { %w(repository_size) }
+ let(:statistics) { %w[repository_size] }
let(:lease_key) { "namespace:namespaces_root_statistics:#{project.namespace_id}" }
describe '#perform' do